For online information and ordering of this and other Manning books, please visit www.manning.com. The publisher offers discounts on this book when ordered in quantity.
No part of this publication may be reproduced, stored in a retrieval system, or transmitted, in any form or by means electronic, mechanical, photocopying, or otherwise, without prior written permission of the publisher.
Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in the book, and Manning Publications was aware of a trademark claim, the designations have been printed in initial caps or all caps.
Recognizing the importance of preserving what has been written, it is Manning’s policy to have the books we publish printed on acid-free paper, and we exert our best efforts to that end. Recognizing also our responsibility to conserve the resources of our planet, Manning books are printed on paper that is at least 15 percent recycled and processed without the use of elemental chlorine.
曼宁出版公司
Manning Publications Co.
鲍德温路 20 号
20 Baldwin Road
邮政信箱 761
PO Box 761
纽约州庇护岛 11964
Shelter Island, NY 11964
收购编辑:Mike Stephens
Acquisitions editor: Mike Stephens
开发编辑:Marina Michaels 和 Dan Maharry
Development editors: Marina Michaels and Dan Maharry
“Realistic examples keep the big picture in focus … A real treat.”
— 格伦布洛克微软
— Glenn Block Microsoft
“写得好、深思熟虑、易于理解,而且……经久不衰。”
“Well-written, thoughtful, easy to follow, and … timeless.”
— 大卫·巴里科尔·纽德西奇
— David Barkol Neudesic
“满足 .NET 设计人员的巨大需求。”
“Fills a huge need for .NET designers.”
— Paul Grebenc PCA 服务
— Paul Grebenc PCA Services
“从一个神秘的话题中解开谜团。”
“Takes the mystery out of a mystifying topic.”
— Rama Krishna 3C 软件
— Rama Krishna 3C Software
“一种深入学习现代软件开发原则的独特个人方式。强烈推荐!”
“A uniquely personal way to learn about modern software development principles in depth. Highly recommended!”
— 达伦·奈姆克 HomeStart Finance
— Darren Neimke HomeStart Finance
“你需要知道的关于依赖注入的一切......以及更多!”
“All you ever need to know about dependency injection ... and more!”
— 乔纳斯·班迪 TechTalk
— Jonas Bandi TechTalk
“必须阅读依赖注入。”
“A must read on Dependency Injection.”
— Braj Panda 凯捷印度
— Braj Panda Capgemini India
“这本书将成为 .NET 堆栈依赖注入的权威指南。”
“This book will be the definitive guide to Dependency Injection for the .NET stack.”
— Doug Ferguson 改善企业
— Doug Ferguson Improving Enterprises
前言
preface
有一种与 Microsoft 相关的奇特现象,称为Microsoft Echo Chamber。Microsoft 是一个庞大的组织,而周围的 Microsoft 认证合作伙伴生态系统使该组织的规模成倍增长。如果你充分融入这个生态系统,就很难超越它的界限。每当您寻找 Microsoft 产品或技术问题的解决方案时,您很可能会找到涉及向其投入更多 Microsoft 产品的答案。无论您在回声室中大喊大叫,答案都是微软!
There’s a peculiar phenomenon related to Microsoft called the Microsoft Echo Chamber. Microsoft is a huge organization, and the surrounding ecosystem of Microsoft Certified Partners multiplies that size by orders of magnitude. If you’re sufficiently embedded in this ecosystem, it can be hard to see past its boundaries. Whenever you look for a solution to a problem with a Microsoft product or technology, you’re likely to find an answer that involves throwing even more Microsoft products at it. No matter what you yell within the echo chamber, the answer is Microsoft!
当 Microsoft 于 2003 年聘用我(马克)时,我已经牢牢地融入了回声室,为 Microsoft 认证合作伙伴工作了多年——我喜欢它!他们很快将我送往新奥尔良的内部技术会议,了解最新最好的 Microsoft 技术。
When Microsoft hired me (Mark) in 2003, I was already firmly embedded in the echo chamber, having worked for Microsoft Certified Partners for years—and I loved it! They soon shipped me off to an internal tech conference in New Orleans to learn about the latest and greatest Microsoft technology.
今天,我不记得我参加过的任何 Microsoft 产品会议,但我记得最后一天。那天,由于未能体验任何可以满足我对酷技术的渴望的课程,我主要是期待飞回丹麦的家。我的首要任务是找个地方坐下,这样我就可以处理我的电子邮件了,所以我选择了一个似乎与我无关的会议并启动了我的笔记本电脑。
Today, I can’t recall any of the Microsoft product sessions I attended—but I do remember the last day. On that day, having failed to experience any sessions that could satisfy my hunger for cool tech, I was mostly looking forward to flying home to Denmark. My top priority was to find a place to sit so I could attend to my email, so I chose a session that seemed marginally relevant for me and fired up my laptop.
会议结构松散,有几位主持人。其中一个是名叫 Martin Fowler 的大胡子,他谈到了测试驱动开发 (TDD) 和动态模拟。我从没听说过他,也没有仔细听,但我心里一定有什么印象。
The session was loosely structured and featured several presenters. One was a bearded guy named Martin Fowler, who talked about Test-Driven Development (TDD) and dynamic mocks. I had never heard of him, and I didn’t listen very closely, but something must have stuck in my mind.
Soon after returning to Denmark, I was tasked with rewriting a big ETL (extract, transform, load) system from scratch, and I decided to give TDD a try (it turned out to be a very good decision). The use of dynamic mocks followed naturally, but also introduced the need to manage dependencies. I found that to be a very difficult but very captivating problem, and I couldn’t stop thinking about it.
What started as a side effect of my interest in TDD became a passion in itself. I did a lot of research, read lots of blog posts about the matter, wrote quite a few blogs myself, experimented with code, and discussed the topic with anyone who cared to listen. Increasingly, I had to look outside the Microsoft Echo Chamber for inspiration and guidance. Along the way, people associated me with the ALT.NET movement even though I was never very active in it. I made all the mistakes it was possible to make, but I was gradually able to develop a coherent understanding of Dependency Injection (DI).
当 Manning 向我提出关于 .NET 中的依赖注入的书的想法时,我的第一反应是,“这有必要吗?” 我觉得开发人员理解 DI 所需的所有概念已经在许多博客文章中进行了描述。有什么要补充的吗?老实说,我认为 .NET 中的 DI 是一个已经死了的主题。
When Manning approached me with the idea for a book about Dependency Injection in .NET, my first reaction was, “Is this even necessary?” I felt that all the concepts a developer needs to understand DI were already described in numerous blog posts. Was there anything to add? Honestly, I thought DI in .NET was a topic that had been done to death already.
然而,经过深思熟虑,我突然意识到,虽然知识肯定是存在的,但它非常分散并且使用了很多相互矛盾的术语。在本书第一版之前,没有关于 DI 的书名试图对它进行连贯的描述。进一步思考后,我意识到 Manning 为我提供了一个巨大的挑战和一个很好的机会来收集和系统化我对 DI 的所有了解。
Upon reflection, however, it dawned on me that while the knowledge is definitely out there, it’s very scattered and uses a lot of conflicting terminology. Before the first edition of this book, there were no titles about DI that attempted to present a coherent description of it. After thinking about it further, I realized that Manning was offering me a tremendous challenge and a great opportunity to collect and systematize all that I knew about DI.
结果就是这本书及其前身——第一版。它使用 .NET Core 和 C# 来介绍和描述 DI 的全面术语和指南,但我希望本书的价值能够超越平台。我认为这里阐述的模式语言是通用的。无论您是 .NET 开发人员还是使用其他面向对象的平台,我希望本书能帮助您成为更好的软件工程师。
The result is this book and its predecessor—the first edition. It uses .NET Core and C# to introduce and describe a comprehensive terminology and guidance for DI, but I hope the value of this book will reach well beyond the platform. I think the pattern language articulated here is universal. Whether you’re a .NET developer or use another object-oriented platform, I hope this book will help you be a better software engineer.
Gratitude may seem like a cliché, but this is only because it’s such a fundamental part of human nature. While we were writing the book, many people gave us good reasons to be grateful, and we would like to thank them all.
首先,在业余时间写一本书让我们对这样的项目对婚姻和家庭生活的负担有多大有了新的认识。Mark的妻子Cecilie全程陪在身边,积极支持他。最重要的是,她明白这个项目对他来说有多重要。他们还在一起,马克期待能有更多时间陪伴她和他们的孩子 Linea 和 Jarl。史蒂文的妻子朱迪思给了他完成这项艰巨任务所需的空间,但她当然很高兴这个项目终于完成了。
First of all, writing a book in our spare time has given us a new understanding of just how taxing such a project is on marriage and family life. Mark’s wife Cecilie stayed with him and actively supported him during the whole process. Most significantly, she understood just how important this project was to him. They’re still together, and Mark looks forward to being able to spend more time with her and their kids Linea and Jarl. Steven’s wife Judith gave him the space needed to complete this immense undertaking, but she certainly is glad that the project is finally finished.
在更专业的层面上,我们要感谢曼宁给我们这个机会。Michael Stephens 发起了这个项目。Dan Maharry、Marina Michaels 和 Christina Taylor 担任我们的开发编辑,并密切关注文本的质量。他们帮助我们找出手稿中的弱点,并提供了广泛的建设性批评。
On a more professional level, we want to thank Manning for giving us this opportunity. Michael Stephens initiated the project. Dan Maharry, Marina Michaels, and Christina Taylor served as our development editors and kept a keen eye on the quality of the text. They helped us identify weak spots in the manuscript and provided extensive constructive criticism.
Karsten Strøbæk 担任我们的技术开发编辑,通读了许多早期草稿,并提供了很多有用的反馈。Mark 撰写第一版时 Karsten 就在场,并在当时担任制作期间的技术校对员。在这一版中,技术校对由 Chris Heneghan 完成,他在整个手稿中发现了许多细微的错误和不一致之处。
Karsten Strøbæk served as our technical development editor, read through numerous early drafts, and provided much helpful feedback. Karsten was there when Mark wrote the first edition and served as the technical proofreader during production at that time. In this edition, technical proofreading was done by Chris Heneghan, who caught many subtle bugs and inconsistencies throughout the manuscript.
写完稿子,我们就进入了制作环节。这是由 Anthony Calcara 管理的。在此过程中,Frances Buran 是我们的文字编辑,而 Nichole Beard 则密切关注本书的图形和图表。
After we were done writing the manuscript, we entered the production process. This was managed by Anthony Calcara. During that process, Frances Buran was our copyeditor, while Nichole Beard held a close watch on the book’s graphics and diagrams.
The following reviewers read the manuscript at various stages of development, and we’re grateful for their comments and insight: Ajay Bhosale, Björn Nordblom, Cemre Mengu, Dennis Sellinger, Emanuele Origgi, Ernesto Cardenas Cangahuala, Gustavo Gomes, Igor Kochetov, Jeremy Caney, Justin Coulston, Mikkel Arentoft, Pasquale Zirpoli, Robert Morrison, Sergio Romero, Shawn Lam, and Stephen Byrne. Reviewing was made possible by Ivan Martinovic, the book’s review editor.
Many of the participants in the Manning Early Access Program (MEAP) also provided feedback and asked difficult questions that exposed the weak parts of the text.
特别感谢 Jeremy Caney,他最初是 MEAP 参与者,后来被提升为审稿人。他为我们提供了大量的反馈,包括语言和上下文。他对 DI 和软件设计的深刻理解非常宝贵。
Special thanks go out to Jeremy Caney, who started out as a MEAP participant but was promoted to reviewer. He supplied us with an immense amount of feedback, both linguistic and contextual. His deep understanding of DI and software design was invaluable.
Also special thanks to Ric Slappendel. Ric advised us on how to compose UWP applications using DI. His knowledge about WPF, UWP, and XAML saved us countless hours and sleepless nights, and completely shaped section 7.2 and its companion code examples. Without Ric’s help, we likely would’ve ended up with a book that didn’t discuss UWP at all.
Alex Meyer-Gleaves and Travis Illig reviewed early versions of chapter 13 and provided us with feedback on using the new Autofac configuration and Decorator support. We’re grateful for their participation.
And finally, Mogens Heller Grabe courteously allowed us to use his picture of a hair dryer wired directly into a wall outlet.
关于这本书
about this book
这是一本关于依赖注入(DI)的书,首先也是最重要的。它也是一本关于 .NET 的书,但它的重要性要小得多。尽管 C# 用于代码示例,但本书中的大部分讨论都可以轻松应用于其他语言和平台。事实上,我们通过阅读以 Java 或 C++ 为例的书籍,了解了很多底层原理和模式。
This is a book about Dependency Injection (DI), first and foremost. It’s also a book about .NET, but that’s much less important. Although C# is used for code examples, much of the discussion in this book can be easily applied to other languages and platforms. In fact, we learned a lot of the underlying principles and patterns from reading books where Java or C++ was used in examples.
DI 是一组相关的模式和原则。它是一种思考和设计代码的方式,而不仅仅是一种特定的技术。使用 DI 的最终目的是在面向对象的范例中创建可维护的软件。
DI is a set of related patterns and principles. It’s a way to think about and design code, more than it is a specific technology. The ultimate purpose of using DI is to create maintainable software within the object-oriented paradigm.
本书中使用的概念都与面向对象编程有关。DI 解决的问题(代码可维护性)是普遍的,但建议的解决方案是在静态类型语言的面向对象编程范围内给出的:C#、Java、Visual Basic .NET、C++ 等。您不能将 DI 应用于过程编程,它可能不是函数式或动态语言中的最佳解决方案。
The concepts used throughout this book all relate to object-oriented programming. The problem that DI addresses (code maintainability) is universal, but the proposed solution is given within the scope of object-oriented programming in statically typed languages: C#, Java, Visual Basic .NET, C++, and so on. You can’t apply DI to procedural programming, and it may not be the best solution in functional or dynamic languages.
孤立的 DI 只是一件小事,但它与面向对象软件设计的大量复杂原则和模式紧密交织在一起。尽管本书自始至终始终专注于 DI,但它还根据 DI 可以提供的特定视角讨论了许多其他主题。本书的目标不仅仅是教您有关 DI 的细节:目标是让您成为更好的面向对象的程序员。
DI in isolation is just a small thing, but it’s closely interwoven with a large complex of principles and patterns for object-oriented software design. Whereas the book focuses consistently on DI from start to finish, it also discusses many of these other topics in the light of the specific perspective that DI can give. The goal of the book is more than just teaching you about DI specifics: the goal is to make you a better object-oriented programmer.
谁应该读这本书?
Who should read this book?
人们很想说这是一本面向所有 .NET 开发人员的书。但今天的 .NET 社区非常庞大,涵盖了使用 Web 应用程序、桌面应用程序、智能手机、RIA、集成、办公自动化、内容管理系统甚至游戏的开发人员。尽管 .NET 是面向对象的,但并非所有开发人员都编写面向对象的代码。
It would be tempting to state that this is a book for all .NET developers. But the .NET community today is vast and spans developers working with web applications, desktop applications, smartphones, RIA, integration, office automation, content management systems, and even games. Although .NET is object oriented, not all of those developers write object-oriented code.
This is a book about object-oriented programming, so at a minimum readers should be interested in object orientation and understand what an interface is. A few years of professional experience and knowledge of design patterns or SOLID principles will certainly be of benefit as well. In fact, we don’t expect beginners to get much out of the book; it’s mostly targeted toward experienced developers and software architects.
The examples are all written in C#, so readers working with other .NET languages must be able to read and understand C#. Readers familiar with non-.NET object-oriented languages like Java and C++ may also find the book valuable, because the .NET platform-specific content is relatively light. Personally, we read a lot of pattern books with examples in Java and still get a lot out of them, so we hope the converse is true as well.
The contents of this book are divided into four parts. Ideally, we’d like you to first read it from cover to cover and then subsequently use it as a reference, but we understand if you have other priorities. For that reason, a majority of the chapters are written so that you can dive right in and start reading from that point.
第一部分是主要例外。它包含对 DI 的一般介绍,最好按顺序阅读。第二部分是模式等的目录,而第三部分也是最大的部分是从三个不同角度检查 DI。本书的第四部分是三个DI Container库的目录。
The first part is the major exception. It contains a general introduction to DI and is probably best read sequentially. The second part is a catalog of patterns and the like, whereas the third and largest part is an examination of DI from three different angles. The fourth part of the book is a catalog of three DI Container libraries.
There are a lot of interconnected concepts, and, because we introduce them the first time it feels natural, this means we often mention concepts before we’ve formally introduced them. To distinguish these universal concepts from more local terms, we consistently use Small Caps to make them stand out. All these terms are briefly defined in the glossary, which also contains references to a more extensive description.
第 1 部分是对 DI 的一般介绍。如果您不知道什么是 DI,请从这里开始;但即使你这样做了,你也可能想要熟悉第 1 部分的内容,因为它建立了本书其余部分使用的大量上下文和术语。第 1 章讨论了 DI 的目的和好处,并提供了一个大纲。第 2 章包含一个大型且相当全面的紧耦合代码示例,第 3 章解释了如何使用 DI 重新实现相同的示例。与其他部分相比,第 1 部分的内容更为线性。您需要从头开始阅读每一章,以便从中获得最大收益。
Part 1 is a general introduction to DI. If you don’t know what DI is, this is the place to start; but even if you do, you may want to familiarize yourself with the contents of part 1, as it establishes a lot of the context and terminology used in the rest of the book. Chapter 1 discusses the purpose and benefits of DI and provides a general outline. Chapter 2 contains a big and rather comprehensive example of tightly coupled code, and chapter 3 explains how to reimplement the same example using DI. Compared to the other parts, part 1 has a more linear progression of its content. You’ll need to read each chapter from the beginning to gain the most from it.
第 2 部分是模式、反模式和代码味道的目录。在这里您可以找到关于如何实施 DI 和需要注意的危险的规范性指导。第 4 章是 DI 设计模式目录,相反,第 5 章是反模式目录。第 6 章包含常见问题的通用解决方案。作为一个目录,每一章都包含一组松散相关的部分,这些部分旨在单独阅读以及按顺序阅读。
Part 2 is a catalog of patterns, anti-patterns, and code smells. This is where you’ll find prescriptive guidance on how to implement DI and the dangers to look out for. Chapter 4 is a catalog of DI design patterns, and, conversely, chapter 5 is a catalog of anti-patterns. Chapter 6 contains generalized solutions to commonly occurring issues. As a catalog, each chapter contains a set of loosely related sections that are designed to be read in isolation as well as in sequence.
Part 3 examines DI from three different angles: Object Composition, Lifetime Management, and Interception. In chapter 7, we discuss how to implement DI on top of existing application frameworks—ASP.NET Core and UWP—and how to implement DI using a console application. Chapter 8 describes how to manage Dependency lifetimes to avoid resources leaks. Whereas the structure is a little less stringent than previous chapters, a large part of that chapter can be used as a catalog of well-known Lifestyles. The remaining three chapters describe how to compose applications with Cross-Cutting Concerns. Chapter 9 goes into the basics of Interception using Decorators, whereas chapters 10 and 11 dive deep into the concept of Aspect-Oriented Programming. This is where you harvest the benefits of all the work that came before, so, in many ways, we consider this to be the climax of the book.
Part 4 is a catalog of DI Container libraries. It starts with a discussion on what DI Containers are and how they fit into the overall picture. The remaining three chapters each cover a specific container in a fair amount of detail: Autofac, Simple Injector, and Microsoft.Extensions.DependencyInjection. Each chapter covers its container in a rather condensed form to save space, so you may want to read about only the one or two containers that interest you the most. In many ways, we regard these three chapters as a very big set of appendixes.
为了使 DI 原则和模式的讨论不涉及任何特定的容器 API,本书的大部分内容(第 4 部分除外)都没有引用特定的容器。这也是容器在第 4 部分中以如此强大的力量出现的原因。我们希望通过保持一般性的讨论,本书将在更长的时间内有用。
To keep the discussion of DI principles and patterns free of any specific container APIs, most of the book, with the exception of part 4, is written without referencing a particular container. This is also why the containers appear with such force in part 4. It’s our hope that by keeping the discussion general, the book will be useful for a longer period of time.
You can also take the concepts from parts 1 through 3 and apply them to container libraries not covered in part 4. There are good containers available that, unfortunately, we couldn’t cover. But even for users of these libraries, we hope that this book has a lot to offer.
代码约定和下载
Code conventions and downloads
本书中有很多代码示例。其中大部分使用 C#,但也有一些 XML 和 JSON。列表和文本中的源代码与 fixed-width font like this普通文本分开。
There are many code examples in this book. Most of those are in C#, but there’s also a bit of XML and JSON here and there. Source code in listings and text is in a fixed-width font like this to separate it from ordinary text.
本书的所有源代码均使用 C# 和 Visual Studio 2017 编写。ASP.NET Core 应用程序是针对 ASP.NET Core v2.1 编写的。
All the source code for the book is written in C# and Visual Studio 2017. The ASP.NET Core applications are written against ASP.NET Core v2.1.
本书中描述的技术中只有少数依赖于现代语言的特性。我们希望在保守和现代编码风格之间取得合理的平衡。当我们专业地编写代码时,我们会在更大程度上使用现代语言功能,但在大多数情况下,最高级的功能是泛型和 LINQ。我们最不想让你明白 DI 只能应用于超现代语言。
Only a few of the techniques described in this book hinge on modern language features. We wanted to strike a reasonable balance between conservative and modern coding styles. When we write code professionally, we use modern language features to a far greater degree, but, for the most part, the most advanced features are generics and LINQ. The last thing we want is for you to get the idea that DI can only be applied with ultra-modern languages.
为一本书编写代码示例会带来一系列挑战。与现代计算机显示器相比,一本书只允许非常短的代码行。用简短但隐晦的方法和变量名称编写简洁风格的代码是非常诱人的。即使您附近有 IDE 和调试器,这样的代码也已经很难理解为真正的代码,但在书本中很难理解。我们发现尽可能保持名称的可读性非常重要。为了使一切都合适,我们有时不得不求助于一些非正统的换行符。所有代码都可以编译,但有时格式看起来有点滑稽。
Writing code examples for a book presents its own set of challenges. Compared to a modern computer monitor, a book only allows for very short lines of code. It was very tempting to write code in a terse style with short but cryptic names for methods and variables. Such code is already difficult to understand as real code even when you have an IDE and a debugger nearby, but it becomes really difficult to follow in a book. We found it very important to keep names as readable as possible. To make it all fit, we’ve sometimes had to resort to some unorthodox line breaks. All the code compiles, but sometimes the formatting looks a bit funny.
The code also makes use of the C# var keyword. In our professional code, where line width isn’t limited by the size of a book’s page, we often use a different coding style when applying var. Here, to save space, we use var whenever we judge that an explicit declaration makes the code less readable.
The word class is often used as a synonym for a type. In .NET, classes, structs, interfaces, enums, and so on are all types, but because the word type is also a word with a lot of overloaded meaning in ordinary language, it would often make the text less clear if used.
Most of the code in this book relates to an overarching example running through the book: an online store complete with supporting internal management applications. This is about the least exciting example you can expect to see in any software text, but we chose it for a few reasons:
对于大多数读者来说,这是一个众所周知的问题领域。虽然这看起来很无聊,但我们认为这是一个优势,因为它不会从 DI 中抢走焦点。
It’s a well-known problem domain for most readers. Although it may seem boring, we think this is an advantage, because it doesn’t steal focus from DI.
我们还必须承认,我们真的想不出任何其他领域足以支持我们想到的所有不同场景。
We also have to admit that we couldn’t really think of any other domain that was rich enough to support all the different scenarios we had in mind.
We wrote a lot of code to support the code examples, and most of that code isn’t in this book. In fact, we wrote almost all of it using Test-Driven Development (TDD), but as this isn’t a TDD book, we generally don’t show the unit tests in the book.
Manning’s commitment to our readers is to provide a venue where a meaningful dialogue between individual readers and between readers and the authors can take place. It isn’t a commitment to any specific amount of participation on the part of the authors, whose contribution to the forum remains voluntary (and unpaid). We suggest that you ask them some challenging questions lest their interest stray! The book forum and the archives of previous discussions will be accessible from the publisher’s website as long as the book is in print.
关于作者
about the authors
Steven van Deursen 是一位荷兰自由职业者 .NET 开发人员和架构师,自 2002 年以来一直在该领域工作。他住在奈梅亨,喜欢为乐趣和利润而编写代码。除了编写代码之外,Steven 还练习武术,喜欢外出就餐,当然也喜欢上一杯威士忌。
Steven van Deursen is a Dutch freelance .NET developer and architect with experience in the field since 2002. He lives in Nijmegen and enjoys writing code for fun and profit. Besides writing code, Steven trains in martial arts, likes to go out for food, and certainly fancies a good whiskey.
Mark Seemann 是一名程序员、软件架构师和演讲者,居住在丹麦哥本哈根。他从 1995 年开始从事软件工作,从 2003 年开始从事 TDD,包括在 Microsoft 工作六年,担任顾问、开发人员和架构师。Mark 目前专业从事软件开发,在哥本哈根工作。他喜欢阅读、绘画、弹吉他、美酒和美食。
Mark Seemann is a programmer, software architect, and speaker living in Copenhagen, Denmark. He has been working with software since 1995 and TDD since 2003, including six years with Microsoft as a consultant, developer, and architect. Mark is currently professionally engaged with software development and is working out of Copenhagen. He enjoys reading, painting, playing the guitar, good wine, and gourmet food.
On the cover of Dependency Injection Principles, Practices, and Patterns is “A woman from Vodnjan,” a small town in the interior of the peninsula of Istria in the Adriatic Sea, off Croatia. The illustration is taken from a reproduction of an album of Croatian traditional costumes from the mid-nineteenth century by Nikola Arsenovic, published by the Ethnographic Museum in Split, Croatia, in 2003. The illustrations were obtained from a helpful librarian at the Ethnographic Museum in Split, itself situated in the Roman core of the medieval center of the town: the ruins of Emperor Diocletian’s retirement palace from around AD 304. The book includes finely colored illustrations of figures from different regions of Croatia, accompanied by descriptions of the costumes and of everyday life. Vodnjan is a culturally and historically significant town, situated on a hilltop with a beautiful view of the Adriatic and known for its many churches and treasures of sacral art. The woman on the cover wears a long, black linen skirt and a short, black jacket over a white linen shirt. The jacket is trimmed with blue embroidery, and a blue linen apron completes the costume. The woman is also wearing a large-brimmed black hat, a flowered scarf, and big hoop earrings. Her elegant costume indicates that she is an inhabitant of the town, rather than a village. Folk costumes in the surrounding countryside are more colorful, made of wool, and decorated with rich embroidery.
Dress codes and lifestyles have changed over the last 200 years, and the diversity by region, so rich at the time, has faded away. It is now hard to tell apart the inhabitants of different continents, let alone of different hamlets or towns separated by only a few miles. Perhaps we have traded cultural diversity for a more varied personal life—certainly for a more varied and fast-paced technological life.
Manning celebrates the inventiveness and initiative of the computer business with book covers based on the rich diversity of regional life of two centuries ago, brought back to life by illustrations from old books and collections like this one.
Dependency Injection (DI) is one of the most misunderstood concepts of object-oriented programming. The confusion is abundant and spans terminology, purpose, and mechanics. Should it be called Dependency Injection, Dependency Inversion, Inversion of Control, or even Third-Party Connect? Is the purpose of DI only to support unit testing, or is there a broader purpose? Is DI the same as Service Location? Do we need DI Containers to apply DI?
有很多讨论 DI 的博客文章、杂志文章、会议报告等,但不幸的是,其中许多使用了相互矛盾的术语或给出了错误的建议。整个行业都是如此,甚至像微软这样有影响力的大公司也加剧了混乱。
There are plenty of blog posts, magazine articles, conference presentations, and so on that discuss DI, but, unfortunately, many of them use conflicting terminology or give bad advice. This is true across the board, and even big and influential actors like Microsoft add to the confusion.
它不必是这样的。在本书中,我们呈现并使用一致的术语。在大多数情况下,我们采用并阐明了其他人定义的现有术语,但偶尔也会添加一些以前不存在的术语。这极大地帮助我们发展了 DI 范围或边界的规范。
It doesn’t have to be this way. In this book, we present and use a consistent terminology. For the most part, we’ve adopted and clarified existing terminology defined by others, but, occasionally, we add a bit of terminology where none existed previously. This has helped us tremendously in evolving a specification of the scope or boundaries of DI.
所有不一致和糟糕建议背后的根本原因之一是 DI 的界限非常模糊。DI 在哪里结束,其他面向对象的概念从哪里开始?我们认为不可能在 DI 和编写好的面向对象代码的其他方面之间划清界限。要谈论 DI,我们必须引入其他概念,例如SOLID、Clean Code,甚至Aspect-Oriented Programming。我们不认为我们可以在不涉及其他一些主题的情况下可靠地撰写有关 DI 的文章。
One of the underlying reasons behind all the inconsistency and bad advice is that the boundaries of DI are quite blurry. Where does DI end, and where do other object-oriented concepts begin? We think that it’s impossible to draw a distinct line between DI and other aspects of writing good object-oriented code. To talk about DI, we have to pull in other concepts such as SOLID, Clean Code, and even Aspect-Oriented Programming. We don’t feel that we can credibly write about DI without also touching on some of these other topics.
本书的第一部分帮助您了解 DI 相对于软件工程其他方面的位置——可以说,将其放在地图上。第 1 章让您快速浏览 DI,涵盖其目的、原则和好处,以及提供本书其余部分范围的大纲。它着眼于大局,并没有涉及很多细节。如果您想了解什么是 DI 以及为什么您应该对它感兴趣,那么这里就是您的起点。本章假设您之前没有 DI 知识。即使您已经了解 DI,您可能仍想阅读它——结果可能与您的预期不同。
The first part of the book helps you understand the place of DI in relation to other facets of software engineering — putting it on the map, so to speak. Chapter 1 gives you a quick tour of DI, covering its purpose, principles, and benefits, as well as providing an outline of the scope for the rest of the book. It’s focused on the big picture and doesn’t go into a lot of details. If you want to learn what DI is and why you should be interested in it, this is the place to start. This chapter assumes you have no prior knowledge of DI. Even if you already know about DI, you may still want to read it — it may turn out to be something other than what you expected.
另一方面,第 2 章和第 3 章完全留给一个大例子。此示例旨在让您对 DI 有更具体的感受。为了将 DI 与更传统的编程风格进行对比,第 2 章展示了示例电子商务应用程序的典型、紧密耦合的实现。第 3 章随后用 DI 重新实现它。
Chapters 2 and 3, on the other hand, are completely reserved for one big example. This example is intended to give you a much more concrete feel for DI. To contrast DI with a more traditional style of programming, chapter 2 showcases a typical, tightly coupled implementation of a sample e-commerce application. Chapter 3 then subsequently reimplements it with DI.
In this part, we’ll discuss DI in general terms. This means we won’t use any so-called DI Container. It’s entirely possible to apply DI without using a DI Container. A DI Container is a helpful, but optional, tool. So parts 1, 2, and 3 more or less ignore DI Containers completely, and instead discuss DI in a container-agnostic way. Then, in part 4, we return to DI Containers to dissect three specific libraries.
第 1 部分为本书的其余部分建立了上下文。它面向没有任何 DI 知识的读者,但经验丰富的 DI 从业者也可以通过浏览各章来了解整本书中使用的术语,从而从中受益。到第 1 部分结束时,您应该牢牢掌握词汇和整体概念,即使某些具体细节仍然有些模糊。没关系——随着您的阅读,本书变得更加具体,因此第 2、3 和 4 部分应该回答您在阅读第 1 部分后可能会遇到的问题。
Part 1 establishes the context for the rest of the book. It’s aimed at readers who don’t have any prior knowledge of DI, but experienced DI practitioners can also benefit from skimming the chapters to get a feeling for the terminology used throughout the book. By the end of part 1, you should have a firm grasp of the vocabulary and overall concepts, even if some of the concrete details are still a little fuzzy. That’s OK — the book becomes more concrete as you read on, so parts 2, 3, and 4 should answer the questions you’re likely to have after reading part 1.
1
依赖注入的基础知识:什么、为什么以及如何
1
The basics of Dependency Injection: What, why, and how
在这一章当中
In this chapter
消除关于依赖注入的常见误解
Dispelling common myths about Dependency Injection
You may have heard that making a sauce béarnaise is difficult. Even among people who regularly cook, many have never attempted to make one. This is a shame, because the sauce is delicious. (It’s traditionally paired with steak, but it’s also an excellent accompaniment to white asparagus, poached eggs, and other dishes.) Some resort to substitutes like ready-made sauces or instant mixes, but these aren’t nearly as satisfying as the real thing.
A sauce béarnaise is an emulsified sauce made from egg yolk and butter, that’s flavored with tarragon, chervil, shallots, and vinegar. It contains no water. The biggest challenge to making it is that its preparation can fail. The sauce can curdle or separate, and, if either happens, you can’t resurrect it. It takes about 45 minutes to prepare, so a failed attempt means that you may not have time for a second try. On the other hand, any chef can prepare a sauce béarnaise. It’s part of their training and, as they’ll tell you, it’s not difficult.
You don’t have to be a professional cook to make sauce béarnaise. Anyone learning to make it will fail at least once, but after you get the hang of it, you’ll succeed every time. We think Dependency Injection (DI) is like sauce béarnaise. It’s assumed to be difficult, and, if you try to use it and fail, it’s likely there won’t be time for a second attempt.
Despite the fear, uncertainty, and doubt (FUD) surrounding DI, it’s as easy to learn as making a sauce béarnaise. You may make mistakes while you learn, but once you’ve mastered the technique, you’ll never again fail to apply it successfully.
Stack Overflow, the software development Q&A website, features an answer to the question, “How to explain Dependency Injection to a 5-year old?” The most highly rated answer, by John Munsch, provides a surprisingly accurate analogy targeted at the (imaginary) five-year-old inquisitor:1
When you go and get things out of the refrigerator for yourself, you can cause problems. You might leave the door open, you might get something Mommy or Daddy doesn’t want you to have. You might even be looking for something we don’t even have or which has expired.
你应该做的是陈述需求,“我需要在午餐时喝点东西,”然后我们会确保你坐下来吃饭时有东西喝。
What you should be doing is stating a need, “I need something to drink with lunch,” and then we will make sure you have something when you sit down to eat.
这在面向对象的软件开发方面意味着:协作类(五岁)应该依赖基础设施(父母)来提供必要的服务。
What this means in terms of object-oriented software development is this: collaborating classes (the five-year-old) should rely on infrastructure (the parents) to provide necessary services.
本章在结构上相当线性。首先,我们介绍 DI,包括它的目的和好处。尽管我们包含示例,但总的来说,本章的代码少于本书中的任何其他章节。在介绍 DI 之前,我们先讨论 DI 的基本目的——可维护性。这很重要,因为如果您没有做好充分准备,很容易误解 DI。接下来,在一个例子(你好 DI!)之后,我们讨论了好处和范围,为本书制定了路线图。完成本章后,您应该为本书其余部分中更高级的概念做好准备。
This chapter is fairly linear in structure. First, we introduce DI, including its purpose and benefits. Although we include examples, overall, this chapter has less code than any other chapter in the book. Before we introduce DI, we discuss the basic purpose of DI — maintainability. This is important because it’s easy to misunderstand DI if you aren’t properly prepared. Next, after an example (Hello DI!), we discuss benefits and scope, laying out a road map for the book. When you’re done with this chapter, you should be prepared for the more advanced concepts in the rest of the book.
To most developers, DI may seem like a rather backward way of creating source code, and, like sauce béarnaise, there’s much FUD involved. To learn about DI, you must first understand its purpose.
1.1 编写可维护的代码
1.1 Writing maintainable code
DI 的作用是什么?DI 本身不是目标;相反,它是达到目的的一种手段。归根结底,大多数编程技术的目的是尽可能高效地交付可工作的软件。其中一方面是编写可维护的代码。
What purpose does DI serve? DI isn’t a goal in itself; rather, it’s a means to an end. Ultimately, the purpose of most programming techniques is to deliver working software as efficiently as possible. One aspect of that is to write maintainable code.
Unless you only write prototypes, or applications that never make it past their first release, you find yourself maintaining and extending existing code bases. To work effectively with such code bases, in general, the more maintainable they are, the better.
An excellent way to make code more maintainable is through loose coupling. As far back as 1994, when the Gang of Four wrote Design Patterns, this was already common knowledge:2
针对接口而不是实现编程。
Program to an interface, not an implementation.
这条重要的建议不是结论,而是设计模式的前提。松耦合使代码可扩展,可扩展性使其可维护。DI 只不过是一种支持松散耦合的技术。此外,关于 DI 存在许多误解,有时会妨碍正确理解。在你可以学习之前,你必须忘记你已经知道的(你认为的)东西。
This important piece of advice isn’t the conclusion, but, rather, the premise of Design Patterns. Loose coupling makes code extensible, and extensibility makes it maintainable. DI is nothing more than a technique that enables loose coupling. Moreover, there are many misconceptions about DI, and sometimes they get in the way of proper understanding. Before you can learn, you must unlearn what (you think) you already know.
1.1.1 关于 DI 的常见误解
1.1.1 Common myths about DI
您以前可能从未遇到或听说过 DI,这很好。跳过本节,直接进入 1.1.2 节。但是,如果您正在阅读本书,您可能至少在谈话中、在您继承的代码库中或在博客文章中遇到过它。您可能还注意到它带有相当多的严厉意见。在本节中,我们将探讨多年来出现的关于 DI 的四种最常见的误解,以及它们为何不正确。这些神话包括以下内容:
You may never have come across or heard of DI before, and that’s great. Skip this section and go straight to section 1.1.2. But, if you’re reading this book, it’s likely you’ve at least come across it in conversation, in a code base you inherited, or in blog posts. You may also have noticed that it comes with a fair amount of heavy opinions. In this section, we’re going to look at four of the most common misconceptions about DI that have appeared over the years and why they aren’t true. These myths include the following:
DI 仅与后期绑定相关。
DI is only relevant for late binding.
DI 仅与单元测试相关。
DI is only relevant for unit testing.
DI 是一种类固醇抽象工厂。
DI is a sort of Abstract Factory on steroids.
DI 需要一个DI Container。
DI requires a DI Container.
尽管这些神话都不是真的,但它们仍然很流行。在您开始了解 DI 之前,我们需要消除它们。
Although none of these myths are true, they’re prevalent nonetheless. We need to dispel them before you can start to learn about DI.
In this context, late binding refers to the ability to replace parts of an application without recompiling the code. An application that enables third-party add-ins (such as Visual Studio) is one example. Another example is the standard software that supports different runtime environments.
Suppose you have an application that runs on more than one database engine (for example, one that supports both Oracle and SQL Server). To support this feature, the rest of the application talks to the database through an interface. The code base provides different implementations of this interface to access Oracle and SQL Server, respectively. In this case, you can use a configuration option to control which implementation should be used for a given installation.
一种常见的误解是 DI 仅与此类场景相关。这是可以理解的,因为 DI 支持这种情况。但谬论是认为这种关系是对称的。DI 启用后期绑定这一事实并不意味着它只与后期绑定场景相关。如图1.1所示,后期绑定只是 DI 的众多方面之一。
It’s a common misconception that DI is only relevant for this sort of scenario. That’s understandable, because DI enables this scenario. But the fallacy is to think that the relationship is symmetric. The fact that DI enables late binding doesn’t mean that it’s only relevant in late-binding scenarios. As figure 1.1 illustrates, late binding is only one of the many aspects of DI.
Figure 1.1 Late binding is enabled by DI, but to assume that it’s only applicable in late-binding scenarios is to adopt a narrow view of a much broader vista.
如果您认为 DI 仅与后期绑定场景相关,那么您需要忘却这一点。DI 的作用远不止启用后期绑定。
If you thought that DI was only relevant for late-binding scenarios, this is something you need to unlearn. DI does much more than enable late binding.
单元测试
Unit testing
有些人认为 DI 只与支持单元测试有关。这也不是真的,尽管 DI 肯定是支持单元测试的重要部分。说实话,我们最初对 DI 的介绍来自于与测试驱动开发的某些方面的斗争(测试驱动开发). 在那段时间里,我们发现了 DI 并了解到其他人已经使用它来支持我们正在处理的一些相同场景。
Some people think that DI is only relevant for supporting unit testing. This isn’t true, either, although DI is certainly an important part of support for unit testing. To tell you the truth, our original introduction to DI came from struggling with certain aspects of Test-Driven Development (TDD). During that time, we discovered DI and learned that other people had used it to support some of the same scenarios we were addressing.
即使您不编写单元测试(如果您不这样做,您应该现在就开始),DI 仍然很重要,因为它提供了所有其他好处。声称 DI 仅与支持单元测试相关就像声称它仅与支持后期绑定相关一样。图 1.2显示虽然这是一个不同的视图,但它是一个与图 1.1一样狭窄的视图。在本书中,我们将尽力向您展示全貌。
Even if you don’t write unit tests (if you don’t, you should start now), DI is still relevant because of all the other benefits it offers. Claiming that DI is only relevant for supporting unit testing is like claiming that it’s only relevant for supporting late binding. Figure 1.2 shows that although this is a different view, it’s a view as narrow as figure 1.1. In this book, we’ll do our best to show you the whole picture.
图 1.2 也许您一直假设单元测试是 DI 的唯一目的。尽管该假设与后期绑定假设的观点不同,但它也是更广阔前景的狭隘观点。
Figure 1.2 Perhaps you’ve been assuming that unit testing is the sole purpose of DI. Although that assumption is a different view than the late-binding assumption, it, too, is a narrow view of a much broader vista.
如果您认为 DI 仅与单元测试相关,请忘掉这个假设。DI 的作用远不止启用单元测试。
If you thought that DI was only relevant for unit testing, unlearn this assumption. DI does much more than enable unit testing.
类固醇的抽象工厂
An Abstract Factory on steroids
也许最危险的谬误是 DI 涉及某种通用的抽象工厂,您可以使用它来创建应用程序所需的依赖项的实例。
Perhaps the most dangerous fallacy is that DI involves some sort of general-purpose Abstract Factory that you can use to create instances of the Dependencies needed in your applications.
在本章的介绍中,我们写道“协作类应该依赖基础设施来提供必要的服务”。你对这句话的最初想法是什么?您是否将基础设施视为某种服务,您可以通过查询来获取所需的依赖项?如果是这样,你并不孤单。许多开发人员和架构师将 DI 视为可用于定位其他服务的服务。这称为服务定位器,但它与 DI 正好相反。
In the introduction to this chapter, we wrote that “collaborating classes should rely on infrastructure to provide necessary services.” What were your initial thoughts about this sentence? Did you think about infrastructure as some sort of service you could query to get the Dependencies you need? If so, you aren’t alone. Many developers and architects think about DI as a service that can be used to locate other services. This is called a Service Locator, but it’s the exact opposite of DI.
A Service Locator is often called an Abstract Factory on steroids because, compared to a normal Abstract Factory, the list of resolvable types is unspecified and possibly endless. It typically has one method allowing the creation of all sorts of types, much like in the following:
public interface IServiceLocator
{
object GetService(Type serviceType);
}
DI容器
DI Containers
与先前的误解密切相关的是 DI 需要DI Container的概念。如果您之前错误地认为 DI 涉及Service Locator,那么很容易得出结论,DI Container可以承担Service Locator的责任。可能是这种情况,但这根本不是您应该如何使用DI Container。
Closely associated with the previous misconception is the notion that DI requires a DI Container. If you held the previous, mistaken belief that DI involves a Service Locator, then it’s easy to conclude that a DI Container can take on the responsibility of the Service Locator. This might be the case, but it’s not at all how you should use a DI Container.
DI 容器是一个可选的库,它可以让您在连接应用程序时更轻松地组合类,但这绝不是必需的。当您在没有DI Container的情况下编写应用程序时,它被称为Pure DI。这可能需要更多的工作,但除此之外,您不必在任何 DI 原则上妥协。
A DI Container is an optional library that makes it easier to compose classes when you wire up an application, but it’s in no way required. When you compose applications without a DI Container, it’s called Pure DI. It might take a little more work, but other than that, you don’t have to compromise on any DI principles.
We have yet to explain exactly what a DI Container is, and how and when you should use it. We’ll go into more detail on this at the end of chapter 3; part 4 is completely dedicated to it.
您可能会认为,虽然我们已经揭露了关于 DI 的四个神话,但我们还没有对其中任何一个提出令人信服的理由。这是真的。从某种意义上说,这本书是对这些常见误解的有力论证,因此我们稍后肯定会回到这些主题。例如,在第 5 章中,5.2 节讨论了为什么使用Service Locator是一种反模式。
You may think that, although we’ve exposed four myths about DI, we have yet to make a compelling case against any of them. That’s true. In a sense, this book is one big argument against these common misconceptions, so we’ll certainly return to these topics later. For example, in chapter 5, section 5.2 discusses why Service Locator is an anti-pattern.
根据我们的经验,忘掉学习是至关重要的,因为人们经常试图改造我们告诉他们的关于 DI 的内容,并将其与他们认为自己已经知道的内容保持一致。当这种情况发生时,他们需要一段时间才能最终意识到他们的一些最基本的假设是错误的。我们不想让你有这种经历。如果可以,请像对 DI 一无所知一样阅读本书。
In our experience, unlearning is vital because people often try to retrofit what we tell them about DI and align it with what they think they already know. When this happens, it takes time before it finally dawns on them that some of their most basic assumptions are wrong. We want to spare you that experience. If you can, read this book as though you know nothing about DI.
1.1.2 理解 DI 的目的
1.1.2 Understanding the purpose of DI
DI 不是最终目标——它是达到目的的手段。DI 实现了松散耦合,松散耦合使代码更易于维护。这是一个相当大的说法,虽然我们可以将您推荐给像四人帮这样的权威机构以了解详细信息,但我们发现解释为什么这是真的是公平的。
DI isn’t an end goal — it’s a means to an end. DI enables loose coupling, and loose coupling makes code more maintainable. That’s quite a claim, and although we could refer you to well-established authorities like the Gang of Four for details, we find it only fair to explain why this is true.
To get this message across, the next section compares software design and several software design patterns with electrical wiring. We’ve found this to be a powerful analogy. We even use it to explain software design to non-technical people.
我们在这个类比中使用了四种特定的设计模式,因为它们经常与 DI 相关。在整本书中,您会看到其中三种模式的许多示例——装饰器、合成器和适配器。(我们将在第 4 章介绍第四种模式,即 Null Object 模式。)如果您对这些模式还不是很熟悉,请不要担心:您将在本书的最后熟悉这些模式。
We use four specific design patterns in this analogy because they occur frequently in relation to DI. You’ll see many examples of three of these patterns — Decorator, Composite, and Adapter — throughout this book. (We cover the fourth, the Null Object pattern, in chapter 4.) Don’t worry if you’re not that familiar with these patterns: you will be by the end of the book.
Software development is still a rather new profession, so in many ways we’re still figuring out how to implement good architecture. But individuals with expertise in more traditional professions (such as construction) figured it out a long time ago.
If you’re staying at a cheap hotel, you might encounter a sight like the one in figure 1.3. Here, the hotel has kindly provided a hair dryer for your convenience, but apparently they don’t trust you to leave the hair dryer for the next guest: the appliance is directly attached to the wall outlet. The hotel management decided that the cost of replacing stolen hair dryers is high enough to justify what’s otherwise an obviously inferior implementation.
Figure 1.3 In a cheap hotel room, you might find a hair dryer wired directly into the wall outlet. This is equivalent to using the common practice of writing tightly coupled code.
What happens when the hair dryer stops working? The hotel has to call in a skilled professional. To fix the hardwired hair dryer, the power to the room will have to be cut, rendering it temporarily useless. Then, the technician must use special tools to disconnect the hair dryer and replace it with a new one. If you’re lucky, the technician will remember to turn the power to the room back on and go back to test whether the new hair dryer works — if you’re lucky. Does this procedure sound at all familiar?
This is how you would approach working with tightly coupled code. In this scenario, the hair dryer is tightly coupled to the wall, and you can’t easily modify one without impacting the other.
Usually, we don’t wire electrical appliances together by attaching the cable directly to the wall. Instead, as in figure 1.4, we use plugs and sockets. A socket defines a shape that the plug must match.
In an analogy to software design, the socket is an interface, and the plug with its appliance is an implementation. This means that the room (the application) has one or (hopefully) more sockets, and the users of the room (the developers) can plug in appliances as they please, potentially even a customer-supplied hair dryer.
In contrast to the hardwired hair dryer, plugs and sockets define a loosely coupled model for connecting electrical appliances. As long as the plug (the implementation) fits into the socket (implements the interface), and it can handle the amount of volts and hertz (obeys the interface contract), we can combine appliances in a variety of ways. What’s particularly interesting is that many of these common combinations can be compared to well-known software design principles and patterns.
First, we’re no longer constrained to hair dryers. If you’re an average reader, we would guess that you need power for a computer much more than you do for a hair dryer. That’s not a problem: you unplug the hair dryer and plug a computer into the same socket (figure 1.5).
Figure 1.5 Using a socket and a plug, you can replace the original hair dryer from figure 1.4 with a computer. This corresponds to the Liskov Substitution Principle.
You can unplug the computer if you don’t need to use it at the moment. Even though nothing is plugged in, the room doesn’t explode. That is to say, if you unplug the computer from the wall, neither the wall outlet nor the computer breaks down.
With software, however, a client often expects a service to be available. If you remove the service, you get a NullReferenceException. To deal with this type of situation, you can create an implementation of an interface that does nothing. This design pattern, known as Null Object, corresponds to having a children’s safety outlet plug (a plug without a wire or appliance that still fits into the socket). And because you’re using loose coupling, you can replace a real implementation with something that does nothing without causing trouble. This is illustrated in figure 1.6.
Figure 1.6 Unplugging the computer causes neither room nor computer to explode when replaced with a children’s safety outlet plug. This can be roughly likened to the Null Object pattern.
There are many other things you can do, as well. If you live in a neighborhood with intermittent power failures, you may want to keep the computer running by plugging in into an uninterrupted power supply (UPS). As shown in figure 1.7, you connect the UPS to the wall outlet and the computer to the UPS.
The computer and the UPS serve separate purposes. Each has a Single Responsibility that doesn’t infringe on the other unit. The UPS and computer are likely to be produced by two different manufacturers, bought at different times, and plugged in separately. As figure 1.5 demonstrated, you can run the computer without a UPS, and you could also conceivably use the hair dryer during blackouts by plugging it into the UPS.
In software design, this way of intercepting one implementation with another implementation of the same interface is known as the Decorator design pattern.5 It gives you the ability to incrementally introduce new features and Cross-Cutting Concerns without having to rewrite or change a lot of existing code.
Another way to add new functionality to an existing code base is to refactor an existing implementation of an interface with a new implementation. When you aggregate several implementations into one, you use the Composite design pattern.6Figure 1.8 illustrates how this corresponds to plugging diverse appliances into a power strip.
The power strip has a single plug that you can insert into a single socket, and the power strip itself provides several sockets for a variety of appliances. This enables you to add and remove the hair dryer while the computer is running. In the same way, the Composite pattern makes it easy to add or remove functionality by modifying the set of composed interface implementations.
Here’s a final example. You sometimes find yourself in situations where a plug doesn’t fit into a particular socket. If you’ve traveled to another country, you’ve likely noticed that sockets differ across the world. If you bring something like the camera in figure 1.9 along when traveling, you’ll need an adapter to charge it. Appropriately, there’s a design pattern with the same name.
Figure 1.9 When traveling, you often need to use an adapter to plug an appliance into a foreign socket (for example, to recharge a camera). This corresponds to the Adapter design pattern. Sometimes, translation is as simple as changing the shape of the plug, or as complex as changing the electric current from alternating current (AC) to direct current (DC).
The Adapter design pattern works like its physical namesake.7 You can use it to match two related, yet separate, interfaces to each other. This is particularly useful when you have an existing third-party API that you want to expose as an instance of an interface your application consumes. As with the physical adapter, implementations of the Adapter design pattern can range from simple to extremely complex.
What’s amazing about the socket and plug model is that, over decades, it’s proven to be an easy and versatile model. Once the infrastructure is in place, it can be used by anyone and adapted to changing needs and unanticipated requirements. What’s even more interesting is that, when we relate this model to software development, all the building blocks are already in place in the form of design principles and patterns.
The advantage of loose coupling is the same in software design as it is in the physical socket and plug model: Once the infrastructure is in place, it can be used by anyone and adapted to changing needs and unforeseen requirements without requiring large changes to the application code base and infrastructure. This means that ideally, a new requirement should only necessitate the addition of a new class, with no changes to other already-existing classes of the system.
This concept of being able to extend an application without modifying existing code is called the Open/Closed Principle. It’s impossible to get to a situation where 100% of your code will always be open for extensibility and closed for modification. Still, loose coupling does bring you closer to that goal.
And, with every step, it gets easier to add new features and requirements to your system. Being able to add new features without touching existing parts of the system means that problems are isolated. This leads to code that’s easier to understand and test, allowing you to manage the complexity of your system. That’s what loose coupling can help you with, and that’s why it can make a code base much more maintainable. We’ll discuss the Open/Closed Principle in more detail in chapter 4.
By now you might be wondering how these patterns will look when implemented in code. Don’t worry about that. As we stated before, we’ll show you plenty of examples of those patterns throughout this book. In fact, later in this chapter, we’ll show you an implementation of both the Decorator and Adapter patterns.
松散耦合的简单部分是针对接口而不是实现进行编程。问题是,“实例来自哪里?” 从某种意义上说,这就是整本书的主题:这是 DI 试图回答的核心问题。
The easy part of loose coupling is programming to an interface instead of an implementation. The question is, “Where do the instances come from?” In a sense, this is what this entire book is about: it’s the core question that DI seeks to answer.
您不能像创建具体类型的新实例那样创建接口的新实例。像这样的代码无法编译:
You can’t create a new instance of an interface the same way that you create a new instance of a concrete type. Code like this doesn’t compile:
接口不包含任何实现,因此这是不可能的。writer实例_必须使用不同的机制创建。DI 解决了这个问题。有了这个 DI 目的的概述,我们认为您已经准备好举个例子了。
An interface contains no implementation, so this isn’t possible. The writer instance must be created using a different mechanism. DI solves this problem. With this outline of the purpose of DI, we think you’re ready for an example.
In the tradition of innumerable programming textbooks, let’s take a look at a simple console application that writes “Hello DI!” to the screen. Note that the full code is available as part of the download for this book, as mentioned in the section “Code conventions and downloads” at the beginning of this book.
In this section, we’ll show you what the code looks like and briefly outline some key benefits without going into details. In the rest of the book, we’ll get more specific.
1.2.1 你好迪!代码
1.2.1 Hello DI! code
您可能习惯于看到用一行代码编写的 Hello World 示例。在这里,我们将把一些非常简单的东西变得更复杂。为什么?我们很快就会谈到这一点,但让我们先看看 Hello World 使用 DI 会是什么样子。
You’re probably used to seeing Hello World examples that are written with a single line of code. Here, we’ll take something that’s extremely simple and make it more complicated. Why? We’ll get to that shortly, but let’s first see what Hello World would look like with DI.
合作者
Collaborators
为了了解程序的结构,我们将从查看Main方法开始控制台应用程序。然后我们将向您展示协作类;但首先,这是MainHello DI 的方法!应用:
To get a sense of the structure of the program, we’ll start by looking at the Main method of the console application. Then we’ll show you the collaborating classes; but first, here’s the Main method of the Hello DI! application:
private static void Main()
{
IMessageWriter writer = new ConsoleMessageWriter();
var salutation = new Salutation(writer);
salutation.Exclaim();
}
Because the program needs to write to the console, it creates a new instance of ConsoleMessageWriter that encapsulates that functionality. It passes that message writer to the Salutation class so that the salutation instance knows where to write its messages. Because everything is now wired up properly, you can execute the logic via the Exclaim method, which results in the message being written to the screen.
The construction of objects inside the Main method is a basic example of Pure DI. No DI Container is used to compose the Salutation and its ConsoleMessageWriterDependency. Figure 1.10 shows the relationship between the collaborators.
Listing 1.1Salutation class encapsulates the main application logic
public class Salutation
{
private readonly IMessageWriter writer;
public Salutation(IMessageWriter writer) ①
{
if (writer == null) ② throw new ArgumentNullException("writer"); ②
this.writer = writer;
}
public void Exclaim()
{
this.writer.Write("Hello DI!"); ③
}
}
The Salutation class depends on a custom interface called IMessageWriter (defined next). It requests an instance of it through its constructor. This practice is called Constructor Injection. A Guard Clause verifies that the supplied IMessageWriter isn’t null by throwing an exception if it is.8 And, finally, you use the previously injected IMessageWriter instance inside the implementation of the Exclaim method by calling its Write method. This sends the Hello DI! message to the IMessageWriterDependency.
用 DI 术语来说,我们说IMessageWriterDependency被注入到Salutation类中使用构造函数参数。请注意,Salutation没有意识。它仅通过界面与之交互。是为该场合定义的简单接口:ConsoleMessageWriterIMessageWriterIMessageWriter
To speak in DI terminology, we say that the IMessageWriterDependency is injected into the Salutation class using a constructor argument. Note that Salutation has no awareness of ConsoleMessageWriter. It interacts with it exclusively through the IMessageWriter interface. IMessageWriter is a simple interface defined for the occasion:
public interface IMessageWriter
{
void Write(string message);
}
It could have had other members, but in this simple example, you only need the Write method. It’s implemented by the ConsoleMessageWriter class that the Main method passes to the Salutation class:
public class ConsoleMessageWriter : IMessageWriter
{
public void Write(string message)
{
Console.WriteLine(message);
}
}
The ConsoleMessageWriter class implements IMessageWriter by wrapping the Console class of the .NET Base Class Library (BCL). This is a simple application of the Adapter design pattern that we talked about in section 1.1.2.
You may be wondering about the benefit of replacing a single line of code with two classes and an interface, resulting in 28 lines total. You could easily solve the same problem as shown here:
DI 可能看起来有点矫枉过正,但使用它有几个好处。前面的示例与通常用于在 C# 中实现 Hello World 的单行代码相比有何优势?在此示例中,DI 增加了 2800% 的开销,但是,随着复杂性从一行代码增加到数万行,这种开销会减少甚至消失。第 3 章提供了一个更复杂的应用 DI 示例。尽管与现实生活中的应用程序相比,该示例仍然过于简单,但您应该注意到 DI 的侵入性要小得多。
DI might seem like overkill, but there are several benefits to be harvested from using it. How is the previous example better than the usual single line of code you normally use to implement Hello World in C#? In this example, DI adds an overhead of 2800%, but, as complexity increases from one line of code to tens of thousands, this overhead diminishes and all but disappears. Chapter 3 provides a more complex example of applied DI. Although that example is still overly simplistic compared to real-life applications, you should notice that DI is far less intrusive.
如果您发现前面的 DI 示例设计过度,我们不会责怪您,但考虑一下:就其本质而言,经典的 Hello World 示例是一个简单的问题,具有明确指定和约束的要求。在现实世界中,软件开发从来都不是这样的。需求不断变化,而且常常是模糊的。您必须实现的功能也往往要复杂得多。DI 通过启用松散耦合来帮助解决此类问题。具体来说,您将获得表 1.1中列出的好处。
We don’t blame you if you find the previous DI example to be over-engineered, but consider this: by its nature, the classic Hello World example is a simple problem with well-specified and constrained requirements. In the real world, software development is never like this. Requirements change and are often fuzzy. The features you must implement also tend to be much more complex. DI helps address such issues by enabling loose coupling. Specifically, you gain the benefits listed in table 1.1.
We listed the late-binding benefit first because, in our experience, this is the one that’s foremost in most people’s minds. When architects and developers fail to understand the benefits of loose coupling, it’s most likely because they never consider the other benefits.
后期绑定
Late binding
当我们向接口和 DI 解释编程的好处时,将一种服务换成另一种服务的能力对大多数人来说是最显着的好处,因此他们往往只考虑这个好处来权衡利弊。还记得我们建议您在学习之前可能需要忘却吗?您可能会说您非常了解自己的需求,以至于您永远不必用其他任何东西替换,比如说,您的 SQL Server 数据库。但是要求会改变。
When we explain the benefits of programming to interfaces and DI, the ability to swap out one service with another is the most conspicuous benefit for most people, so they tend to weigh the advantages against the disadvantages with only this benefit in mind. Remember when we suggested that you may need to unlearn before you can learn? You may say that you know your requirements so well that you know you’ll never have to replace, say, your SQL Server database with anything else. But requirements change.
In section 1.2.1, you didn’t use late binding because you explicitly created a new instance of IMessageWriter by hard coding the creation of a new ConsoleMessageWriter instance. You can, however, introduce late binding by changing this single line of code:
IMessageWriter writer = new ConsoleMessageWriter();
要启用后期绑定,您可以将该行代码替换为类似以下内容。
To enable late binding, you might replace that line of code with something like the following.
By pulling the type name from the application configuration file and creating a Type instance from it, you can use reflection to create an instance of IMessageWriter without knowing the concrete type at compile time. To make this work, you specify the type name in the messageWriter application setting in the application configuration file:
Loose coupling enables late binding because there’s only a single place where you create the instance of IMessageWriter. Because the Salutation class works exclusively against the IMessageWriter interface, it never notices the difference. In the Hello DI! example, late binding would enable you to write the message to a different destination than the console; for example, a database or a file. It’s possible to add such features — even though you didn’t explicitly plan ahead for them.
Successful software must be able to change. You’ll need to add new features and extend existing features. Loose coupling lets you efficiently recompose the application, similar to the way you have flexibility when working with electrical plugs and sockets.
Let’s say that you want to make the Hello DI! example more secure by only allowing authenticated users to write the message. Listing 1.3 shows how you can add that feature without changing any of the existing features — you simply add a new implementation of the IMessageWriter interface.
Listing 1.3 Extending the Hello DI! application with a security feature
public class SecureMessageWriter : IMessageWriter ①
{
private readonly IMessageWriter writer;
private readonly IIdentity identity;
public SecureMessageWriter(
IMessageWriter writer, ②
IIdentity identity)
{
if (writer == null)
throw new ArgumentNullException("writer");
if (identity == null)
throw new ArgumentNullException("identity");
this.writer = writer;
this.identity = identity;
}
public void Write(string message)
{
if (this.identity.IsAuthenticated) ③
{
this.writer.Write(message); ④
}
}
}
Besides an instance of IMessageWriter, the SecureMessageWriter constructor requires an instance of IIdentity. The Write method is implemented by first checking whether the current user is authenticated, using the injected IIdentity. If this is the case, it allows the decorated writer field to Write the message. The only place where you need to change existing code is in the Main method, because you need to compose the available classes differently than before:
IMessageWriter writer =
new SecureMessageWriter( ①
new ConsoleMessageWriter(),
WindowsIdentity.GetCurrent());
Notice that you wrap or decorate the old ConsoleMessageWriter instance with the new SecureMessageWriter class. Once more, the Salutation class is unmodified because it only consumes the IMessageWriter interface. Similarly, there’s no need to either modify or duplicate the functionality in the ConsoleWriter class, either. You use the System.Security.Principal.WindowsIdentity class to retrieve the identity of the user on whose behalf this code is being executed.10
As we’ve stated before, loose coupling enables you to write code that’s open for extensibility, but closed for modification. The only place where you need to modify the code is at the application entry point. SecureMessageWriter implements the security features of the application, whereas ConsoleMessageWriter addresses the user interface. This enables you to vary these aspects independently of each other and compose them as needed. Each class has its own Single Responsibility.
Separation of concerns makes it possible to develop code in parallel. When a software development project grows to a certain size, it becomes necessary to have multiple developers work in parallel on the same code base. At a larger scale, it’s even necessary to separate the development team into multiple teams of manageable sizes. Each team is often assigned responsibility for an area of the overall application. To demarcate responsibilities, each team develops one or more modules that will need to be integrated into the finished application. Unless the areas of each team are truly independent, some teams are likely to depend on functionality developed by other teams.
In the previous example, because the SecureMessageWriter and ConsoleMessageWriter classes don’t depend directly on each other, they could’ve been developed by parallel teams. All they would have needed to agree on was the shared interface IMessageWriter.
As the responsibility of each class becomes clearly defined and constrained, maintenance of the overall application becomes easier. This is a consequence of the Single Responsibility Principle, which states that each class should have only a single responsibility. We’ll discuss the Single Responsibility Principle in more detail in chapter 2.
Adding new features to an application becomes simpler because it’s clear where changes should be applied. More often than not, you don’t need to change existing code, but can instead add new classes and recompose the application. This is the Open/Closed Principle in action again.
Troubleshooting also tends to become less grueling, because the scope of likely culprits narrows. With clearly defined responsibilities, you’ll often have a good idea of where to start looking for the root cause of a problem.
可测试性
Testability
当一个应用程序可以进行单元测试时,它被认为是可测试的。对于某些人来说,可测试性是他们最不担心的事情;对于其他人来说,这是绝对的要求。就个人而言,我们属于后一类。在 Mark 的职业生涯中,他拒绝了几份工作邀请,因为这些工作涉及使用某些不可测试的产品。
An application is considered Testable when it can be unit tested. For some, Testability is the least of their worries; for others, it’s an absolute requirement. Personally, we belong in the latter category. In Mark’s career, he’s declined several job offers because they involved working with certain products that weren’t Testable.
The benefit of Testability is perhaps the most controversial of those we’ve listed. Some developers and architects still don’t practice unit testing, so they consider this benefit irrelevant at best. We, however, see it as an essential part of software development, which is why we marked it as “Always valuable” in table 1.1. Michael Feathers even defines the term legacy application as any application that isn’t covered by unit tests.11
Almost by accident, loose coupling enables unit testing because consumers follow the Liskov Substitution Principle: they don’t care about the concrete types of their Dependencies. This means that you can inject Test Doubles into the System Under Test (SUT), as you’ll see in listing 1.4.
The ability to replace intended Dependencies with test-specific replacements is a by-product of loose coupling, but we chose to list it as a separate benefit because the derived value is different. Our personal experience is that DI is beneficial even during integration testing. Although integration tests typically communicate with real external systems (like a database), you still need to have a certain degree of isolation. In other words, there are still reasons to replace, Intercept, or mock certain Dependencies in the application being tested.
Depending on the type of application you’re developing, you may or may not care about the ability to do late binding, but we always care about Testability. Some developers don’t care about Testability but find late binding important for the application they’re developing. Regardless, DI provides options in the future with minimal additional overhead today.
In section 1.2.1, you saw the Hello DI! example. Although we showed you the final code first, we developed it using TDD. Listing 1.4 shows the most important unit test.
[Fact]
public void ExclaimWillWriteCorrectMessageToMessageWriter()
{
var writer = new SpyMessageWriter();
var sut = new Salutation(writer); ①
sut.Exclaim();
Assert.Equal(
expected: "Hello DI!",
actual: writer.WrittenMessage);
}
public class SpyMessageWriter : IMessageWriter
{
public string WrittenMessage { get; private set; }
public void Write(string message)
{
this.WrittenMessage += message;
}
}
该类Salutation需要一个接口实例IMessageWriter,因此您需要创建一个。您可以使用任何实现,但在单元测试中,Test Double 可能很有用——在这种情况下,您可以使用自己的 Test Spy 实现。14
The Salutation class needs an instance of the IMessageWriter interface, so you need to create one. You could use any implementation, but in unit tests, a Test Double can be useful — in this case, you roll your own Test Spy implementation.14
在这种情况下,测试替身与生产实施一样复杂。这是我们的示例多么简单的产物。在大多数应用程序中,测试替身比它所代表的具体的生产实现要简单得多。重要的部分是提供特定于测试的实现,IMessageWriter以确保您一次只测试一件事。现在,您正在测试该Exclaim方法Salutation班级的,因此您不希望 的生产实施IMessageWriter污染测试。要创建该类,请传入使用构造函数注入Salutation的 Test Spy 实例IMessageWriter.
In this case, the Test Double is as involved as the production implementation. This is an artifact of how simple our example is. In most applications, a Test Double is significantly simpler than the concrete, production implementations it stands in for. The important part is to supply a test-specific implementation of IMessageWriter to ensure that you test only one thing at a time. Right now, you’re testing the Exclaim method of the Salutation class, so you don’t want a production implementation of IMessageWriter to pollute the test. To create the Salutation class, you pass in the Test Spy instance of IMessageWriter using Constructor Injection.
行使 SUT 后,您可以调用以验证预期结果是否等于实际结果。如果方法Assert.EqualIMessageWriter.Write被调用了“Hello DI!” 字符串,会把它存储在它的属性中SpyMessageWriterWrittenMessage,Equal方法完成。但是,如果该Write方法未被调用,或以不同的值被调用,则该Equal方法会抛出异常,测试会失败。
After exercising the SUT, you can call Assert.Equal to verify whether the expected outcome equals the actual outcome. If the IMessageWriter.Write method was invoked with the "Hello DI!" string, SpyMessageWriter would have stored this in its WrittenMessage property, and the Equal method completes. But if the Write method wasn’t called, or was called with a different value, the Equal method would throw an exception, and the test would fail.
Loose coupling provides many benefits: code becomes easier to develop, maintain, and extend, and it becomes more Testable. It’s not even particularly difficult. We program against interfaces, not concrete implementations. The only major obstacle is to figure out how to get hold of instances of those interfaces. DI surmounts this obstacle by injecting the Dependencies from the outside. Constructor Injection is the preferred method of doing that, though we’ll also explore a few additional options in chapter 4.
1.3 注入什么和不注入什么
1.3 What to inject and what not to inject
在上一节中,我们首先描述了让人想到 DI 的动机。如果您确信松散耦合是一种好处,那么您可能希望使所有内容都松散耦合。总的来说,这是个好主意。当您需要决定如何打包模块时,松散耦合被证明特别有用。但是您不必将所有内容都抽象出来并使其可插入。在本节中,我们将提供一些决策工具来帮助您决定如何对依赖项建模。
In the previous section, we described the motivational forces that makes one think about DI in the first place. If you’re convinced that loose coupling is a benefit, you may want to make everything loosely coupled. Overall, that’s a good idea. When you need to decide how to package modules, loose coupling proves especially useful. But you don’t have to abstract everything away and make it pluggable. In this section, we’ll provide some decision tools to help you decide how to model your Dependencies.
The .NET BCL consists of many assemblies. Every time you write code that uses a type from a BCL assembly, you add a dependency to your module. In the previous section, we discussed how loose coupling is important and how programming to an interface is the cornerstone. Does this imply that you can’t reference any BCL assemblies and use their types directly in your application? What if you’d like to use an XmlWriter that’s defined in the System.Xml assembly?
You don’t have to treat all Dependencies equally. Many types in the BCL can be used without jeopardizing an application’s degree of coupling — but not all of them. It’s important to know how to distinguish between types that pose no danger and types that may tighten an application’s degree of coupling. Focus mainly on the latter.
在学习 DI 时,将依赖项分类为稳定依赖项和易变依赖项会很有帮助。决定将接缝放在哪里将很快成为您的第二天性。下一节将更详细地讨论这些概念。
As you learn DI, it can be helpful to categorize your Dependencies into Stable Dependencies and Volatile Dependencies. Deciding where to put your Seams will soon become second nature to you. The next sections discuss these concepts in more detail.
Many of the modules in the BCL and beyond pose no threat to an application’s degree of modularity. They contain reusable functionality that you can use to make your own code more succinct. The BCL modules are always available to your application, because it needs the .NET Framework to run, and, because they already exist, the concern about parallel development doesn’t apply to these modules. You can always reuse a BCL library in another application.
By default, you can consider most (but not all) types defined in the BCL as safe, or Stable Dependencies. We call them stable because they’re already there, they tend to be backward compatible, and invoking them has deterministic outcomes. Most Stable Dependencies are BCL types, but other Dependencies can be stable too. The important criteria for Stable Dependencies include the following:
类或模块已经存在。
The class or module already exists.
您希望新版本不会包含重大更改。
You expect that new versions won’t contain breaking changes.
所讨论的类型包含确定性算法。
The types in question contain deterministic algorithms.
您永远不会期望必须用另一个替换、包装、装饰或拦截类或模块。
You never expect to have to replace, wrap, decorate, or Intercept the class or module with another.
Other examples may include specialized libraries that encapsulate algorithms relevant to your application. For example, if you’re developing an application that deals with chemistry, you can reference a third-party library that contains chemistry-specific functionality.
一般来说,Dependencies可以被认为是稳定的。如果它们不不稳定,它们就是稳定的。
In general, Dependencies can be considered stable by exclusion. They’re stable if they aren’t volatile.
Introducing Seams into an application is extra work, so you should only do it when it’s necessary. There can be more than one reason it’s necessary to isolate a Dependency behind a Seam, but those reasons are closely related to the benefits of loose coupling (discussed in section 1.2.1).
Such Dependencies can be recognized by their tendency to interfere with one or more of these benefits. They aren’t stable because they don’t provide a sufficient foundation for applications, and we call them Volatile Dependencies for that reason. A Dependency should be considered volatile if any of the following criteria are true:
数据库是 BCL 类型的很好的例子,它们是Volatile Dependencies,而关系数据库是典型的例子。如果你不在Seam背后隐藏一个关系数据库,你就永远无法用任何其他技术取代它。它还使设置和运行自动化单元测试变得困难。(尽管 Microsoft SQL Server 客户端库是 BCL 中包含的一项技术,但它的使用暗示了关系数据库。)其他进程外资源,如消息队列、Web 服务,甚至文件系统都属于这一类。这种类型的依赖的症状是缺乏后期绑定和可扩展性,以及禁用的可测试性。
The Dependency introduces a requirement to set up and configure a runtime environment for the application. It isn’t so much the concrete .NET types that are volatile, but rather what they imply about the runtime environment.
Databases are good examples of BCL types that are Volatile Dependencies, and relational databases are the archetypical example. If you don’t hide a relational database behind a Seam, you can never replace it by any other technology. It also makes it hard to set up and run automated unit tests. (Even though the Microsoft SQL Server client library is a technology contained in the BCL, its usage implies a relational database.) Other out-of-process resources like message queues, web services, and even the filesystem fall into this category. The symptoms of this type of Dependency are lack of late binding and extensibility, as well as disabled Testability.
该依赖项尚不存在,或者仍在开发中。
The Dependency doesn’t yet exist, or is still in development.
The Dependency isn’t installed on all machines in the development organization. This may be the case for expensive third-party libraries or Dependencies that can’t be installed on all operating systems. The most common symptom is disabled Testability.
The Dependency contains nondeterministic behavior. This is particularly important in unit tests because all tests must be deterministic. Typical sources of nondeterminism are random numbers and algorithms that depend on the current date or time.
Because the BCL defines common sources of nondeterminism, such as System.Random, System.Security.Cryptography.RandomNumberGenerator, or System.DateTime.Now, you can’t avoid having a reference to the assembly in which they’re defined. Nevertheless, you should treat them as Volatile Dependencies because they tend to destroy Testability.
现在您了解了稳定依赖项和可变依赖项之间的区别,您可以开始了解 DI 范围的轮廓。松散耦合是一种普遍的设计原则,因此 DI(作为推动者)应该在您的代码库中无处不在。DI 的主题和良好的软件设计之间没有严格的界限,但是为了定义本书其余部分的范围,我们将快速描述它所涵盖的内容。
Now that you understand the differences between Stable and Volatile Dependencies, you can begin to see the contours of the scope of DI. Loose coupling is a pervasive design principle, so DI (as an enabler) should be everywhere in your code base. There’s no hard line between the topic of DI and good software design, but to define the scope of the rest of the book, we’ll quickly describe what it covers.
As we discussed before, an important element of DI is to break up various responsibilities into separate classes. One responsibility that we take away from classes is the task of creating instances of Dependencies. The task of creating instances of Dependencies is referred to as Object Composition.
我们在 Hello DI 中讨论过这个问题!例如,我们的Salutation班级被释放了创建其Dependency的责任。相反,这个责任被转移到应用程序的Main方法. UML 图再次显示在图 1.11中。
We discussed this in our Hello DI! example where our Salutation class was released of the responsibility of creating its Dependency. Instead, this responsibility was moved to the application’s Main method. The UML diagram is shown again in figure 1.11.
As a class relinquishes control of Dependencies, it gives up more than the decision to select particular implementations. By doing this, we, as developers, gain some advantages. At first, it may seem like a disadvantage to let a class surrender control over which objects are created, but we don’t lose that control — we only move it to another place.
Object Composition isn’t the only dimension of control that we remove: a class also loses the ability to control the lifetime of the object. When a Dependency instance is injected into a class, the consumer doesn’t know when it was created, or when it’ll go out of scope. This should be of no concern to the consumer. Making the consumer oblivious to the lifetime of its Dependencies simplifies the consumer.
DI 使您有机会以统一的方式管理依赖项。当消费者直接创建和设置Dependencies的实例时,每个人都可以以自己的方式进行。这可能与其他消费者的做法不一致。您没有办法集中管理依赖关系,也没有简单的方法来解决跨领域问题. 使用 DI,您可以获得拦截每个依赖实例并在它传递给消费者之前对其进行操作的能力。这提供了应用程序的可扩展性。
DI gives you an opportunity to manage Dependencies in a uniform way. When consumers directly create and set up instances of Dependencies, each may do so in its own way. This can be inconsistent with how other consumers do it. You have no way to centrally manage Dependencies and no easy way to address Cross-Cutting Concerns. With DI, you gain the ability to Intercept each Dependency instance and act on it before it’s passed to the consumer. This provides extensibility in applications.
使用 DI,您可以在拦截依赖项并控制其生命周期的同时编写应用程序。Object Composition、Interception和Lifetime Management是 DI 的三个维度。接下来,我们将简要介绍其中的每一个;本书的第 3 部分将进行更详细的处理。
With DI, you can compose applications while intercepting Dependencies and controlling their lifetimes. Object Composition, Interception, and Lifetime Management are three dimensions of DI. Next, we’ll cover each of these briefly; a more detailed treatment follows in part 3 of the book.
To harvest the benefits of extensibility, late binding, and parallel development, you must be able to compose classes into applications. This means that you’ll want to create an application out of individual classes by putting them together, much like plugging electrical appliances together. And, as with electrical appliances, you’ll want to easily rearrange those classes when new requirements are introduced, ideally, without having to make changes to existing classes.
对象组合通常是将 DI 引入应用程序的主要动机。事实上,最初,DI 是Object Composition的同义词;这是 Martin Fowler 关于该主题的原始文章中讨论的唯一方面。16
Object Composition is often the primary motivation for introducing DI into an application. In fact, initially, DI was synonymous with Object Composition; it’s the only aspect discussed in Martin Fowler’s original article on the subject.16
您可以通过多种方式将类组合到应用程序中。当我们讨论后期绑定时,我们使用一个配置文件和一些动态对象实例化来从可用模块手动组合应用程序。我们也可以使用DI Container使用Configuration as Code。我们将在第 12 章回到这些。
You can compose classes into an application in several ways. When we discussed late binding, we used a configuration file and a bit of dynamic object instantiation to manually compose the application from the available modules. We could also have used Configuration as Code using a DI Container. We’ll return to these in chapter 12.
许多人将 DI 称为控制反转(国际奥委会)。这两个术语有时可以互换使用,但 DI 是 IoC 的一个子集。在整本书中,我们始终使用最具体的术语——DI。如果我们指的是 IoC,我们会专门提到它。
Many people refer to DI as Inversion of Control (IoC). These two terms are sometimes used interchangeably, but DI is a subset of IoC. Throughout the book, we consistently use the most specific term — DI. If we mean IoC, we refer to it specifically.
A class that has surrendered control of its Dependencies gives up more than the power to select particular implementations of an Abstraction. It also gives up the power to control when instances are created and when they go out of scope.
In .NET, the garbage collector takes care of these things for us. A consumer can have its Dependencies injected into it and use them for as long as it wants. When it’s done, the Dependencies go out of scope. If no other classes reference them, they’re eligible for garbage collection.
What if two consumers share the same type of Dependency? Listing 1.5 illustrates that you can choose to inject a separate instance into each consumer, whereas listing 1.6 shows that you can alternatively choose to share a single instance across several consumers. But from the perspective of the consumer, there’s no difference. According to the Liskov Substitution Principle, the consumer must treat all instances of a given interface equally.
Listing 1.5 Consumers getting their own instance of the same type of Dependency
IMessageWriter writer1 = new ConsoleMessageWriter(); ① IMessageWriter writer2 = new ConsoleMessageWriter(); ① var salutation = new Salutation(writer1); ② var valediction = new Valediction(writer2); ②
Listing 1.6 Consumers sharing an instance of the same type of Dependency
IMessageWriter writer = new ConsoleMessageWriter(); ① var salutation = new Salutation(writer); ② var valediction = new Valediction(writer); ②
因为可以共享依赖关系,所以单个消费者不可能控制其生命周期。只要托管对象可以超出范围并被垃圾收集,这就不是什么大问题。但是当Dependencies实现IDisposable接口时,事情变得更加复杂,我们将在 8.2 节中讨论。总的来说,终身管理是 DI 的一个独立维度,并且非常重要,我们将第 8 章的所有内容都放在一边。
Because Dependencies can be shared, a single consumer can’t possibly control its lifetime. As long as a managed object can go out of scope and be garbage collected, this isn’t much of an issue. But when Dependencies implement the IDisposable interface, things become much more complicated as we’ll discuss in section 8.2. As a whole, Lifetime Management is a separate dimension of DI and important enough that we’ve set aside all of chapter 8 for it.
When we delegate control over Dependencies to a third party, as figure 1.12 shows, we also provide the power to modify them before we pass them on to the classes consuming them.
在你好 DI!例如,我们最初将一个实例注入到一个实例中。然后,修改示例,我们添加了一个安全功能,方法是创建一个新的,仅在用户通过身份验证时将进一步的工作委托给。这允许您维护单一职责原则ConsoleMessageWriterSalutationSecureMessageWriterConsoleMessageWriter. 这样做是可能的,因为您总是针对接口进行编程;回想一下Dependencies必须始终是Abstractions。对于 the Salutation,它不关心提供IMessageWriter的是 aConsoleMessageWriter还是 a SecureMessageWriter。SecureMessageWritercan wrapConsoleMessageWriter仍然执行实际工作。
In the Hello DI! example, we initially injected a ConsoleMessageWriter instance into a Salutation instance. Then, modifying the example, we added a security feature by creating a new SecureMessageWriter that only delegates further work to the ConsoleMessageWriter when the user is authenticated. This allows you to maintain the Single Responsibility Principle. It’s possible to do this because you always program to interfaces; recall that Dependencies must always be Abstractions. In the case of the Salutation, it doesn’t care whether the supplied IMessageWriter is a ConsoleMessageWriter or a SecureMessageWriter. The SecureMessageWriter can wrap a ConsoleMessageWriter that still performs the real work.
Such abilities of Interception move us along the path towards Aspect-Oriented Programming (AOP), a closely related topic that we’ll cover in chapters 10 and 11. With Interception and AOP, you can apply Cross-Cutting Concerns such as logging, auditing, access control, validation, and so forth in a well-structured manner that lets you maintain Separation of Concerns.
1.4.4 三维中的 DI
1.4.4 DI in three dimensions
尽管 DI 最初是一系列旨在解决对象组合问题的模式,但该术语随后扩展到还涵盖对象生命周期和拦截。今天,我们认为 DI 以一致的方式包含所有这三个方面。
Although DI started out as a series of patterns aimed at solving the problem of Object Composition, the term has subsequently expanded to also cover Object Lifetime and Interception. Today, we think of DI as encompassing all three in a consistent way.
Object Composition tends to dominate the picture because, without flexible Object Composition, there’d be no Interception and no need to manage Object Lifetime. Object Composition has dominated most of this chapter and will continue to dominate this book, but you shouldn’t forget the other aspects. Object Composition provides the foundation, and Lifetime Management addresses some important side effects. But it’s mainly when it comes to Interception that you start to reap the benefits.
In part 3, we’ve devoted a chapter to each dimension briefly mentioned here. But it’s important to know that, in practice, DI is more than Object Composition.
1.5 结论
1.5 Conclusion
依赖注入是达到目的的手段,而不是目标本身。这是实现松耦合的最佳方式,松耦合是可维护代码的重要组成部分。从松散耦合中获得的好处并不总是立即显现出来,但随着代码库复杂性的增加,它们会随着时间的推移变得明显。与 DI 相关的松散耦合的一个重点是,为了有效,它应该在代码库中无处不在。
Dependency Injection is a means to an end, not a goal in itself. It’s the best way to enable loose coupling, an important part of maintainable code. The benefits you can reap from loose coupling aren’t always immediately apparent, but they’ll become visible over time, as the complexity of a code base grows. An important point about loose coupling in relation to DI is that, in order to be effective, it should be everywhere in your code base.
A tightly coupled code base will eventually deteriorate into Spaghetti Code;18 whereas a well-designed, loosely coupled code base can stay maintainable. It takes more than loose coupling to reach a truly supple design,19 but programming to interfaces is a prerequisite.
DI 只不过是设计原则和模式的集合。它更多地是关于一种思考和设计代码的方式,而不是关于工具和技术。DI 的目的是使代码可维护。小型代码库,如经典的 Hello World 示例,由于其大小而具有内在的可维护性。这就是为什么 DI 在简单的例子中往往看起来像过度工程。代码库越大,好处就越明显。我们在接下来的两章中专门介绍了一个更大、更复杂的示例来展示这些好处。
DI is nothing more than a collection of design principles and patterns. It’s more about a way of thinking and designing code than it is about tools and techniques. The purpose of DI is to make code maintainable. Small code bases, like a classic Hello World example, are inherently maintainable because of their size. This is why DI tends to look like overengineering in simple examples. The larger the code base becomes, the more visible the benefits. We’ve dedicated the next two chapters to a larger and more complex example to showcase these benefits.
概括
Summary
依赖注入是一组软件设计原则和模式,使您能够开发松散耦合的代码。松散耦合使代码更易于维护。
Dependency Injection is a set of software design principles and patterns that enables you to develop loosely coupled code. Loose coupling makes code more maintainable.
When you have a loosely coupled infrastructure in place, it can be used by anyone and adapted to changing needs and unanticipated requirements without having to make large changes to the application’s code base and its infrastructure.
故障排除往往会变得不那么费力,因为可能的罪魁祸首的范围缩小了。
Troubleshooting tends to become less taxing because the scope of likely culprits narrows.
DI 支持后期绑定,即无需重新编译原始代码即可将类或模块替换为不同类或模块的能力。
DI enables late binding, which is the ability to replace classes or modules with different ones without the need for the original code to be recompiled.
DI 使代码更容易以未明确计划的方式扩展和重用,类似于您在使用电插头和插座时具有灵活性的方式。
DI makes it easier for code to be extended and reused in ways not explicitly planned for, similar to the way you have flexibility when working with electrical plugs and sockets.
DI简化了同一代码库的并行开发,因为关注点分离允许每个团队成员甚至整个团队更轻松地处理孤立的部分。
DI simplifies parallel development on the same code base because the Separation of Concerns allows each team member or even entire teams to work more easily on isolated parts.
DI 使软件更易于测试,因为您可以在编写单元测试时将依赖项替换为测试实现。
DI makes software more Testable because you can replace Dependencies with test implementations when writing unit tests.
当你练习 DI 时,协作类应该依赖基础设施来提供必要的服务。你可以通过让你的类依赖于接口而不是具体的实现来做到这一点。
When you practice DI, collaborating classes should rely on infrastructure to provide the necessary services. You do this by letting your classes depend on interfaces, instead of concrete implementations.
Classes shouldn’t ask a third party for their Dependencies. This is an anti-pattern called Service Locator. Instead, classes should specify their required Dependencies statically using constructor parameters, a practice called Constructor Injection.
许多开发人员认为 DI 需要专门的工具,即所谓的DI 容器。这是一个神话。DI 容器是一个有用但可选的工具。
Many developers think that DI requires specialized tooling, a so-called DI Container. This is a myth. A DI Container is a useful, but optional, tool.
支持 DI 的最重要的软件设计原则之一是Liskov 替换原则。它允许在不破坏客户端或实现的情况下将接口的一种实现替换为另一种实现。
One of the most important software design principles that enables DI is the Liskov Substitution Principle. It allows replacing one implementation of an interface with another without breaking either the client or the implementation.
Dependencies are considered Stable in the case that they’re already available, have deterministic behavior, don’t require a setup runtime environment (such as a relational database), and don’t need to be replaced, wrapped, or intercepted.
Dependencies are considered Volatile when they are under development, aren’t always available on all development machines, contain nondeterministic behavior, or need to be replaced, wrapped, or intercepted.
易失性依赖项是 DI 的焦点。我们将Volatile Dependencies注入到类的构造函数中。
Volatile Dependencies are the focal point of DI. We inject Volatile Dependencies into a class’s constructor.
By removing control over Dependencies from their consumers, and moving that control into the application entry point, you gain the ability to apply Cross-Cutting Concerns more easily and can manage the lifetime of Dependencies more effectively.
要取得成功,您需要普遍应用 DI。所有类都应该使用Constructor Injection获得所需的Volatile Dependencies。很难将松散耦合和 DI 改造到现有代码库中。
To succeed, you need to apply DI pervasively. All classes should get their required Volatile Dependencies using Constructor Injection. It’s hard to retrofit loose coupling and DI onto an existing code base.
2
编写紧耦合代码
2
Writing tightly coupled code
在这一章当中
In this chapter
编写紧密耦合的应用程序
Writing a tightly coupled application
评估该应用程序的可组合性
Evaluating the composability of that application
分析该应用程序中缺乏可组合性
Analyzing the lack of composability in that application
As we mentioned in chapter 1, a sauce béarnaise is an emulsified sauce made from egg yolk and butter, but this doesn’t magically instill in you the ability to make one. The best way to learn is to practice, but an example can often bridge the gap between theory and practice. Watching a professional cook making a sauce béarnaise is helpful before you try it out yourself.
When we introduced Dependency Injection (DI) in the last chapter, we presented a high-level tour to help you understand its purpose and general principles. But that simple explanation doesn’t do justice to DI. DI is a way to enable loose coupling, and loose coupling is first and foremost an efficient way to deal with complexity.
Most software is complex in the sense that it must address many issues simultaneously. Besides the business concerns, which may be complex in their own right, software must also address matters related to security, diagnostics, operations, performance, and extensibility. Instead of addressing all of these concerns in one big ball of mud, loose coupling encourages you to address each concern separately. It’s easier to address each in isolation, but ultimately, you must still compose this complex set of issues into a single application.
在本章中,我们将看一个更复杂的例子。您将看到编写紧密耦合的代码是多么容易。您还将与我们一起从可维护性的角度分析为什么紧密耦合的代码会出现问题。在第 3 章中,我们将使用 DI 将这一紧耦合的代码库完全重写为松耦合的代码库。如果你想马上看到松散耦合的代码,你可能想跳过这一章。如果没有,当你读完本章后,你应该开始理解是什么导致紧耦合代码如此成问题。
In this chapter, we’ll take a look at a more complex example. You’ll see how easy it is to write tightly coupled code. You’ll also join us in an analysis of why tightly coupled code is problematic from a maintainability perspective. In chapter 3, we’ll use DI to completely rewrite this tightly coupled code base to one that’s loosely coupled. If you want to see loosely coupled code right away, you may want to skip this chapter. If not, when you’re done with this chapter, you should begin to understand what it is that makes tightly coupled code so problematic.
2.1 构建紧耦合应用
2.1 Building a tightly coupled application
构建松散耦合代码的想法并不是特别有争议,但理论与实践之间存在巨大差距。在下一章向您展示如何使用 DI 构建松散耦合的应用程序之前,我们想向您展示它是多么容易出错。松散耦合代码的常见尝试是构建分层应用程序。任何人都可以画出三层应用图,图2.1证明我们也可以。
The idea of building loosely coupled code isn’t particularly controversial, but there’s a huge gap between theory and practice. Before we show you in the next chapter how to use DI to build a loosely coupled application, we want to show you how easily it can go wrong. A common attempt at loosely coupled code is building a layered application. Anyone can draw a three-layer application diagram, and figure 2.1 proves that we can too.
Figure 2.1 Standard three-layer application architecture. This is the simplest and most common variation of the n-layer application architecture, whereby an application is composed of n distinct layers.
Drawing a three-layer diagram is deceptively simple, but the act of drawing the diagram is akin to stating that you’ll have sauce béarnaise with your steak: it’s a declaration of intent that carries no guarantee with regard to the final result. You can end up with something else, as you shall soon see.
There’s more than one way to view and design a flexible and maintainable complex application, but the n-layer application architecture constitutes a well-known, tried-and-tested approach. The challenge is to implement it correctly. Armed with a three-layer diagram like the one in figure 2.1, you can start building an application.
2.1.1 认识玛丽罗文
2.1.1 Meet Mary Rowan
Mary Rowan 是一名专业的 .NET 开发人员,就职于一家主要开发 Web 应用程序的本地 Microsoft 认证合作伙伴。她今年 34 岁,从事软件工作已有 11 年。这使她成为更有经验的开发人员之一在公司里。除了履行作为高级开发人员的常规职责外,她还经常担任初级开发人员的导师。总的来说,Mary 对她所做的工作很满意,但令她感到沮丧的是,里程碑经常被错过,这迫使她和她的同事长时间工作并在周末工作以赶上最后期限。
Mary Rowan is a professional .NET developer working for a local Certified Microsoft Partner that mainly develops web applications. She’s 34 years old and has been working with software for 11 years. This makes her one of the more experienced developers in the company. In addition to performing her regular duties as a senior developer, she often acts as a mentor for junior developers. In general, Mary is happy about the work that she’s doing, but it frustrates her that milestones are often missed, forcing her and her colleagues to work long hours and weekends to meet deadlines.
She suspects that there must be more efficient ways to build software. In an effort to learn about efficiency, she buys a lot of programming books, but she rarely has time to read them, as much of her spare time is spent with her husband and two girls. Mary likes to go hiking in the mountains. She’s also an enthusiastic cook, and she definitely knows how to make a real sauce béarnaise.
Mary 被要求在 ASP.NET Core MVC 和 Entity Framework Core 上创建一个新的电子商务应用程序,并将 SQL Server 作为数据存储。为了最大化模块化,它必须是一个三层应用程序。
Mary has been asked to create a new e-commerce application on ASP.NET Core MVC and Entity Framework Core with SQL Server as the data store. To maximize modularity, it must be a three-layer application.
The first feature to implement should be a simple list of featured products, pulled from a database table and displayed on a web page (an example is shown in figure 2.2). And, if the user viewing the list is a preferred customer, the price on all products should be discounted by 5%.
图 2.2 Mary 被要求开发的电子商务 Web 应用程序的屏幕截图。它具有特色产品及其价格的简单列表。
Figure 2.2 Screen capture of the e-commerce web application Mary has been asked to develop. It features a simple list of featured products and their prices.
为了完成她的第一个功能,Mary 必须执行以下操作:
To complete her first feature, Mary will have to implement the following:
A data layer — Includes a Products table in the database, which represents all database rows, and a Product class, which represents a single database row
领域层— 包含检索特色产品的逻辑
A domain layer — Contains the logic for retrieving the featured products
A UI Layer with an MVC controller — Handles incoming requests, retrieves the relevant data from the domain layer, and sends it to the Razor view, which eventually renders the list of featured products
让我们看看 Mary 在实现应用程序的第一个功能时的工作。
Let’s look over Mary’s shoulder as she implements the application’s first feature.
Because Mary will need to pull data from a database table, she has decided to begin by implementing the data layer. The first step is to define the database table itself. Mary uses SQL Server Management Studio to create the table shown in table 2.1.
public class Product
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal UnitPrice { get; set; }
public bool IsFeatured { get; set; }
}
Mary uses Entity Framework for her data access needs. She adds a dependency to the Microsoft.EntityFrameworkCore.SqlServer NuGet package to her project, and implements an application-specific DbContext class that allows her application to access the Products table via the CommerceContext class. The following listing shows her CommerceContext class.
public class CommerceContext : Microsoft.EntityFrameworkCore.DbContext
{
public DbSet<Product> Products { get; set; } ① protected override void OnConfiguring( ②
DbContextOptionsBuilder builder)
{
var config = new ConfigurationBuilder() ③ .SetBasePath( ③ Directory.GetCurrentDirectory()) ③ .AddJsonFile("appsettings.json") ③ .Build(); ③ string connectionString = ④ config.GetConnectionString( ④ "CommerceConnectionString"); ④ ④ builder.UseSqlServer(connectionString); ④
}
}
因为从配置文件加载连接字符串,所以需要创建该文件。Mary 将一个名为 appsettings.json 的文件添加到她的 Web 项目中,其中包含以下内容:CommerceContext
Because CommerceContext loads a connection string from a configuration file, that file needs to be created. Mary adds a file named appsettings.json to her web project, with the following content:
CommerceContext and Product are public types contained within the same assembly. Mary knows that she’ll later need to add more features to her application, but the data access component required to implement the first feature is now completed (figure 2.3).
Now that the data access layer has been implemented, the next logical step is the domain layer. The domain layer is also referred to as the domain logic layer, business layer, or business logic layer. Domain logic is all the behavior that the application needs to have, specific to the domain the application is built for.
With the exception of pure data-reporting applications, there’s always domain logic. You may not realize it at first, but as you get to know the domain, its embedded and implicit rules and assumptions will gradually emerge. In the absence of any domain logic, the list of products exposed by CommerceContext could technically have been used directly from the UI layer.
Mary 的应用程序要求向优先客户显示产品标价和 5% 的折扣。Mary 还没有弄清楚如何确定首选客户,因此她向她的同事 Jens 寻求建议:
The requirements for Mary’s application state that preferred customers should be shown the product list prices with a 5% discount. Mary has yet to figure out how to identify a preferred customer, so she asks her coworker Jens for advice:
玛丽:我需要实施这个业务逻辑,以便优先客户获得 5% 的折扣。
Mary: I need to implement this business logic so that a preferred customer gets a 5% discount.
延斯:听起来很简单。只需乘以 0.95。
Jens: Sounds easy. Just multiply by .95.
玛丽:谢谢,但这不是我想问你的。我想问你的是,我应该如何识别一个优先客户?
Mary: Thanks, but that’s not what I wanted to ask you about. What I wanted to ask you is, how should I identify a preferred customer?
詹斯:我明白了。这是 Web 应用程序还是桌面应用程序?
Jens: I see. Is this a web application or a desktop application?
Jens: Oh. [Thinks for a while] I still think you should use the HttpContext of ASP.NET to look up the value for the user. You can then pass the value to your domain logic as a boolean.
Jens: That’ll also ensure that you have good Separation of Concerns because your domain logic doesn’t have to deal with security. You know, the Single Responsibility Principle! It’s the Agile way to do it!
玛丽:我想你说得有道理。
Mary: I guess you’ve got a point.
Jens 的建议基于他对 ASP.NET 的技术知识。当讨论把他带离自己的舒适区时,他用流行语的三重组合来压制玛丽。请注意,Jens 并不知道他在说什么:
Jens is basing his advice on his technical knowledge of ASP.NET. As the discussion takes him away from his comfort zone, he steamrolls Mary with a triple combo of buzzwords. Be aware that Jens doesn’t know what he’s talking about:
他滥用了关注点分离的概念. 尽管将安全问题与域逻辑分开很重要,但将其移至表示层无助于分离问题。
He misuses the concept of Separation of Concerns. Although it’s important to separate security concerns from the domain logic, moving this to the presentation layer doesn’t help in separating concerns.
他之所以提到敏捷,是因为他最近听到其他人热情地谈论它。
He only mentions Agile because he recently heard someone else talk enthusiastically about it.
He completely misses the point of the Single Responsibility Principle. Although the quick feedback cycle that Agile methodologies provide can help you improve your software design accordingly, by itself, the Single Responsibility Principle as a software design principle is independent of the chosen software development methodology.
Armed with Jens’ unfortunately poor advice, Mary creates a new C# library project and adds a class called ProductService, shown in listing 2.3. To make the ProductService class compile, she must add a reference to her data access layer, because the CommerceContext class is defined there.
public class ProductService
{
private readonly CommerceContext dbContext;
public ProductService()
{
this.dbContext = new CommerceContext(); ①
}
public IEnumerable<Product> GetFeaturedProducts(
bool isCustomerPreferred)
{
decimal discount =
isCustomerPreferred ? .95m : 1;
var featuredProducts = ② from product in this.dbContext.Products ② where product.IsFeatured ②
select product;
return
from product in ③ featuredProducts.AsEnumerable() ③ select new Product ③ { ③ Id = product.Id, ③ Name = product.Name, ③ Description = product.Description, ③ IsFeatured = product.IsFeatured, ③ UnitPrice = ③ product.UnitPrice * discount ③
};
}
}
Mary 很高兴她在ProductService类中封装了数据访问技术 (Entity Framework Core)、配置和域逻辑。她通过传入isCustomerPreferred参数将用户的知识委托给了调用者,她使用此值计算所有产品的折扣。
Mary’s happy that she has encapsulated the data access technology (Entity Framework Core), configuration, and domain logic in the ProductService class. She has delegated the knowledge of the user to the caller by passing in the isCustomerPreferred parameter, and she uses this value to calculate the discount for all the products.
进一步的改进可能包括用可配置的数字替换硬编码的折扣值 (.95),但就目前而言,这种实现就足够了。玛丽快完成了。唯一剩下的就是 UI 层。玛丽决定可以等到明天。图 2.4显示了 Mary 在实现图 2.1中设想的体系结构方面取得了多大进展。
Further refinement could include replacing the hard-coded discount value (.95) with a configurable number, but, for now, this implementation will suffice. Mary’s almost done. The only thing still left is the UI layer. Mary decides that it can wait until tomorrow. Figure 2.4 shows how far Mary has come with implementing the architecture envisioned in figure 2.1.
Figure 2.4 Compared to figure 2.3, Mary has now implemented the data access layer and the domain layer. The UI layer still remains to be implemented.
Mary 没有意识到的是,通过让依赖项依赖于数据访问层的类,她将她的领域层与数据访问层紧密耦合。我们将在 2.2 节中解释这有什么问题。ProductServiceCommerceContext
What Mary doesn’t realize is that by letting the ProductService depend on the data access layer’s CommerceContext class, she tightly coupled her domain layer to the data access layer. We’ll explain what’s wrong with that in section 2.2.
2.1.4 创建UI层
2.1.4 Creating the UI layer
第二天,Mary 继续使用电子商务应用程序,将新的 ASP.NET Core MVC 应用程序添加到她的解决方案中。如果您不熟悉 ASP.NET Core MVC 框架,请不要担心。MVC 框架如何运作的复杂细节不是本次讨论的重点。重要的部分是如何使用依赖项,这是一个相对平台中立的主题。
The next day, Mary resumes her work with the e-commerce application, adding a new ASP.NET Core MVC application to her solution. Don’t worry if you aren’t familiar with the ASP.NET Core MVC framework. The intricate details of how the MVC framework operates aren’t the focus of this discussion. The important part is how Dependencies are consumed, and that’s a relatively platform-neutral subject.
下一个清单显示了 Mary 如何实现一个Index方法在她的HomeController课堂上从数据库中提取特色产品并将它们传递给视图。为了使这段代码编译通过,她必须添加对数据访问层和域层的引用。这是因为ProductService类是在领域层定义的,而Product类是在数据访问层定义的。
The next listing shows how Mary implements an Index method on her HomeController class to extract the featured products from the database and pass them to the view. To make this code compile, she must add references to both the data access layer and the domain layer. This is because the ProductService class is defined in the domain layer, but the Product class is defined in the data access layer.
Listing 2.4Index method on the default controller class
public ViewResult Index()
{
bool isPreferredCustomer = ① this.User.IsInRole("PreferredCustomer"); ① var service = new ProductService(); ② var products = service.GetFeaturedProducts( ③ isPreferredCustomer); ③ this.ViewData["Products"] = products; ④
return this.View();
}
作为 ASP.NET Core MVC 生命周期的一部分,User属性类上的HomeController会自动填充正确的用户对象,因此 Mary 使用它来确定当前用户是否是首选客户。有了这些信息,她就可以调用域逻辑来获取特色产品列表。
As part of the ASP.NET Core MVC lifecycle, the User property on the HomeController class is automatically populated with the correct user object, so Mary uses it to determine if the current user is a preferred customer. Armed with this information, she can invoke the domain logic to get the list of featured products.
在 Mary 的应用程序中,产品列表必须由Index视图呈现。以下清单显示了视图的标记。
In Mary’s application, the list of products must be rendered by the Index view. The following listing shows the markup for the view.
<h2>Featured Products</h2>
<div>
@{
var products = ① (IEnumerable<Product>)this.ViewData["Products"]; ① foreach (Product product in products) ②
{
<div>@product.Name (@product.UnitPrice.ToString("C"))</div>
}
}
</div>
ASP.NET Core MVC 允许您编写标准 HTML,其中嵌入了一些命令式代码,以访问由创建视图的控制器创建和分配的对象。在这种情况下,HomeController的Index方法将特色产品列表分配给一个名为ProductsMary 在视图中用于呈现产品列表的键。图 2.5显示了 Mary 现在如何实现图 2.1中设想的体系结构。
ASP.NET Core MVC lets you write standard HTML with bits of imperative code embedded to access objects created and assigned by the controller that created the view. In this case, the HomeController’s Index method assigned the list of featured products to a key called Products that Mary uses in the view to render the list of products. Figure 2.5 shows how Mary has now implemented the architecture envisioned in figure 2.1.
Figure 2.5 Mary has now implemented all three layers in the application.
所有三层都到位后,应用程序理论上应该可以正常工作。但只有运行该应用程序,她才能验证情况是否如此。
With all three layers in place, the applications should theoretically work. But only by running the application can she verify whether that’s the case.
2.2 评估紧耦合应用
2.2 Evaluating the tightly coupled application
Mary 现在已经实现了所有三个层,所以是时候看看应用程序是否正常工作了。她按下F5,出现如图2.2所示的网页。Featured Products 功能现已完成,Mary 充满信心并准备好在应用程序中实施下一个功能。毕竟,她遵循既定的最佳实践并创建了一个三层应用程序……或者她做到了吗?
Mary has now implemented all three layers, so it’s time to see if the application works. She presses F5 and the web page shown in figure 2.2 appears. The Featured Products feature is now done, and Mary feels confident and ready to implement the next feature in the application. After all, she followed established best practices and created a three-layer application ... or did she?
Mary 是否成功开发了合适的分层应用程序?不,她没有,尽管她当然是出于好意。她创建了三个 Visual Studio 项目,对应于计划架构中的三个层。对于不经意的观察者来说,这看起来像是梦寐以求的分层架构,但是,正如您将看到的,代码是紧密耦合的。
Did Mary succeed in developing a proper, layered application? No, she didn’t, although she certainly had the best of intentions. She created three Visual Studio projects that correspond to the three layers in the planned architecture. To the casual observer, this looks like the coveted layered architecture, but, as you’ll see, the code is tightly coupled.
Visual Studio 使以这种方式处理解决方案和项目变得简单自然。如果您需要来自不同库的功能,您可以轻松地添加对它的引用并编写代码来创建其他库中定义的类型的新实例。但是,每次添加引用时,您都会承担一个Dependency。
Visual Studio makes it easy and natural to work with solutions and projects in this way. If you need functionality from a different library, you can easily add a reference to it and write code that creates new instances of the types defined in the other libraries. Every time you add a reference, though, you take on a Dependency.
2.2.1 评估依赖图
2.2.1 Evaluating the dependency graph
在 Visual Studio 中使用解决方案时,很容易忘记重要的依赖项。这是因为 Visual Studio 将它们与可能指向 .NET 基类库中的程序集的所有其他项目引用一起显示(BCL)。要了解 Mary 的应用程序中的模块如何相互关联,我们可以绘制依赖关系图(见图 2.6)。
When working with solutions in Visual Studio, it’s easy to lose track of the important Dependencies. This is because Visual Studio displays them together with all the other project references that may point to assemblies in the .NET Base Class Library (BCL). To understand how the modules in Mary’s application relate to each other, we can draw a graph of the dependencies (see figure 2.6).
The most remarkable insight to be gained from figure 2.6 is that the UI layer depends on both domain and data access layers. It seems as though the UI could bypass the domain layer in certain cases. This requires further investigation.
2.2.2 评估可组合性
2.2.2 Evaluating composability
构建三层应用程序的一个主要目标是分离关注点。我们希望将我们的域模型与数据访问层和 UI 层分开,以便这些问题都不会污染域模型。在大型应用程序中,能够独立处理应用程序的每个区域至关重要。为了评估 Mary 的实现,我们可以问一个简单的问题:是否可以单独使用每个模块?
A major goal of building a three-layer application is to separate concerns. We’d like to separate our domain model from the data access and UI layers so that none of these concerns pollute the domain model. In large applications, it’s essential to be able to work with each area of the application in isolation. To evaluate Mary’s implementation, we can ask a simple question: Is it possible to use each module in isolation?
理论上,我们应该能够以任何我们喜欢的方式组合模块。我们可能需要编写新模块以新的和意想不到的方式将现有模块绑定在一起,但理想情况下,我们应该能够这样做而不必修改现有模块。我们能否以令人兴奋的新方式使用 Mary 的应用程序中的模块?让我们看看一些可能的情况。
In theory, we should be able to compose modules any way we like. We may need to write new modules to bind existing modules together in new and unanticipated ways, but, ideally, we should be able to do so without having to modify the existing modules. Can we use the modules in Mary’s application in new and exciting ways? Let’s look at some likely scenarios.
构建新的用户界面
Building a new UI
如果 Mary 的应用程序成功,项目利益相关者希望她在 Windows Presentation Foundation (WPF). 在重用域和数据访问层时可以这样做吗?
If Mary’s application becomes a success, the project stakeholders would like her to develop a rich client version in Windows Presentation Foundation (WPF). Is this possible to do while reusing the domain and data access layers?
当我们检查图 2.6中的依赖关系图时,我们可以快速确定没有模块依赖于 Web UI,因此可以删除它并用 WPF UI 替换它。创建基于 WPF 的富客户端是一个新的应用程序,它与原始 Web 应用程序共享其大部分实现。图 2.7说明了 WPF 应用程序如何需要采用与 Web 应用程序相同的依赖项。原始 Web 应用程序可以保持不变。
When we examine the dependency graph in figure 2.6, we can quickly ascertain that no modules are depending on the web UI, so it’s possible to remove it and replace it with a WPF UI. Creating a rich client based on WPF is a new application that shares most of its implementation with the original web application. Figure 2.7 illustrates how a WPF application would need to take the same dependencies as the web application. The original web application can remain unchanged.
图 2.7 用 WPF UI 替换 Web UI 是可能的,因为没有模块依赖于 Web UI。虚线框表示我们要替换的部分。
Figure 2.7 Replacing a web UI with a WPF UI is possible because no module depends on the web UI. The dashed box signals the part that we want to replace.
Mary 的实现当然可以替换 UI 层。让我们检查另一个有趣的分解。
Replacing the UI layer is certainly possible with Mary’s implementation. Let’s examine another interesting decomposition.
构建新的数据访问层
Building a new data access layer
玛丽的市场分析师发现,为了优化利润,她的应用程序应该作为托管在 Microsoft Azure 上的云应用程序提供. 在蔚蓝,数据可以存储在高度可扩展的 Azure 表存储服务中。这种存储机制基于包含不受约束数据的灵活数据容器。该服务不强制执行特定的数据库模式,也没有参照完整性。
Mary’s market analysts figure out that, to optimize profits, her application should be available as a cloud application hosted on Microsoft Azure. In Azure, data can be stored in the highly scalable Azure Table Storage Service. This storage mechanism is based on flexible data containers that contain unconstrained data. The service enforces no particular database schema, and there’s no referential integrity.
Although the most common data access technology on .NET is based on ADO.NET Data Services, the protocol used to communicate with the Table Storage Service is HTTP. This type of database is sometimes known as a key-value database, and it’s a different beast than a relational database accessed through Entity Framework Core.
要使电子商务应用程序成为云应用程序,必须将数据访问层替换为使用表存储服务的模块。这可能吗?
To enable the e-commerce application as a cloud application, the data access layer must be replaced with a module that uses the Table Storage Service. Is this possible?
从图 2.6的依赖图中,我们已经知道 UI 层和领域层都依赖于基于 Entity Framework 的数据访问层。如果我们尝试删除数据访问层,解决方案将不再编译而不重构所有其他项目,因为缺少必需的依赖项。在一个有几十个模块的大型应用程序中,我们也可以尝试删除不编译的模块,看看会剩下什么。对于 Mary 的应用程序,很明显我们必须删除所有模块,不留下任何东西,如图 2.8所示。
From the dependency graph in figure 2.6, we already know that both the UI and domain layers depend on the Entity Framework–based data access layer. If we try to remove the data access layer, the solution will no longer compile without refactoring all other projects because a required Dependency is missing. In a big application with dozens of modules, we could also try to remove the modules that don’t compile to see what would be left. In the case of Mary’s application, it’s evident that we’d have to remove all modules, leaving nothing behind, as figure 2.8 shows.
Although it would be possible to develop an Azure Table data access layer that mimics the API exposed by the original data access layer, there’s no way we could apply that to the application without touching other parts of the application. The application isn’t nearly as composable as the project stakeholders would have liked. Enabling the profit-maximizing cloud abilities requires a major rewrite of the application because none of the existing modules can be reused.
We could analyze the application for other combinations of modules, but this would be a moot point because we already know that it fails to support an important scenario. Besides, not all combinations make sense.
For instance, we could ask whether it would be possible to replace the domain model with a different implementation. But, in most cases, this would be an odd question to ask because the domain model encapsulates the heart of the application. Without the domain model, most applications have no reason to exist.
2.3 缺失可组合性分析
2.3 Analysis of missing composability
为什么 Mary 的实现未能达到预期的可组合性程度?是不是因为UI直接依赖于数据访问层?让我们更详细地研究这种可能性。
Why did Mary’s implementation fail to achieve the desired degree of composability? Is it because the UI has a direct dependency on the data access layer? Let’s examine this possibility in greater detail.
2.3.1 依赖图分析
2.3.1 Dependency graph analysis
为什么UI依赖于数据访问库?罪魁祸首是这个领域模型的方法签名:
Why does the UI depend on the data access library? The culprit is this domain model’s method signature:
The GetFeaturedProducts method of the ProductService class returns a sequence of products, but the Product class is defined in the data access layer. Any client consuming the GetFeaturedProducts method must reference the data access layer to be able to compile. It’s possible to change the signature of the method to return a type defined within the domain model. It’d also be more correct, but it doesn’t solve the problem.
Let’s assume that we break the dependency between the UI and data access library. The modified dependency graph would now look like figure 2.9.
这样的更改是否能让 Mary 将关系数据访问层替换为封装对 Azure 表服务的访问的层?不幸的是,没有,因为领域层仍然依赖于数据访问层。反过来,UI 仍然依赖于域模型。如果我们试图移除原始数据访问层,应用程序将一无所有。问题的根本原因在别处。
Would such a change enable Mary to replace the relational data access layer with one that encapsulates access to the Azure Table service? Unfortunately, no, because the domain layer still depends on the data access layer. The UI, in turn, still depends on the domain model. If we try to remove the original data access layer, there’d be nothing left of the application. The root cause of the problem lies somewhere else.
The domain model depends on the data access layer because the entire data model is defined there. Using Entity Framework to implement a data access layer may be a reasonable decision. But, from the perspective of loose coupling, consuming it directly in the domain model isn’t.
The offending code can be found spread out in the ProductService class. The constructor creates a new instance of the CommerceContext class and assigns it to a private member variable:
This tightly couples the ProductService class to the data access layer. There’s no reasonable way you can Intercept this piece of code and replace it with something else. The reference to the data access layer is hard-coded into the ProductService class!
The implementation of the GetFeaturedProducts method uses CommerceContext to pull Product objects from the database:
var featuredProducts =
from product in this.dbContext.Products
where product.IsFeatured
select product;
CommerceContext对within的引用GetFeaturedProducts加强了硬编码的依赖性,但此时,损害已经造成。我们需要的是一种没有这种紧密耦合的更好的模块组合方式。如果您回顾第 1 章中讨论的 DI 的好处,您会发现 Mary 的应用程序没有以下内容:
The reference to CommerceContext within GetFeaturedProducts reinforces the hard-coded dependency, but, at this point, the damage is already done. What we need is a better way to compose modules without such tight coupling. If you look back at the benefits of DI as discussed in chapter 1, you’ll see that Mary’s application fails to have the following:
后期绑定 — 由于域层与数据访问层紧密耦合,因此不可能部署同一应用程序的两个版本,其中一个连接到本地 SQL Server 数据库,另一个使用 Azure 表存储托管在 Microsoft Azure 上。换句话说,使用后期绑定加载正确的数据访问层是不可能的。
Late binding — Because the domain layer is tightly coupled with the data access layer, it becomes impossible to deploy two versions of the same application, where one connects to a local SQL Server database and the other is hosted on Microsoft Azure using Azure Table Storage. In other words, it’s impossible to load the correct data access layer using late binding.
Extensibility — Because all classes in the application are tightly coupled to one another, it becomes costly to plug in Cross-Cutting Concerns like the security feature in chapter 1. Doing so requires many classes in the system to be changed. This tightly coupled design is, therefore, not particularly extensible.
可维护性 — 不仅会添加横切关注点需要在整个应用程序中进行彻底的更改,但是每个新添加的横切关注点都可能会使每个涉及的类更加复杂。每次添加都会使课程更难阅读。这意味着该应用程序不像 Mary 所希望的那样易于维护。
Maintainability — Not only would adding Cross-Cutting Concerns require sweeping changes throughout the application, but every newly added Cross-Cutting Concern would likely make each class touched even more complex. Every addition would make a class harder to read. This means that the application isn’t as maintainable as Mary would like.
并行开发 — 如果我们坚持前面应用横切关注点的示例,就很容易理解必须对整个代码库进行全面更改会阻碍与多个开发人员在单个应用程序上并行工作的能力。像我们一样,在过去将您的工作提交到版本控制系统时,您可能已经处理过痛苦的合并冲突。一个设计良好、松散耦合的系统,除其他外,将减少您将拥有的合并冲突的数量。当更多的开发人员开始处理 Mary 的应用程序时,要在不影响彼此的情况下有效地工作会变得越来越难。
Parallel development — If we stick with the previous example of applying Cross-Cutting Concerns, it’s quite easy to understand that having to make sweeping changes throughout your code base hinders the ability to work with multiple developers in parallel on a single application. Like us, you’ve likely dealt with painful merge conflicts in the past when committing your work to a version control system. A well-designed, loosely coupled system will, among other things, reduce the amount of merge conflicts that you’ll have. When more developers start working on Mary’s application, it’ll become harder and harder to work effectively without stepping on each other’s toes.
Testability — We already established that swapping out the data access layer is currently impossible. Testing code without a database, however, is a prerequisite for doing unit testing. But even with integration testing, Mary will likely need some parts of the code to be swapped out, and the current design makes this hard. Mary’s application is, therefore, not Testable.
At this point, you may ask yourself what the desired dependency graph should look like. For the highest degree of reuse, the lowest amount of dependencies is desirable. On the other hand, the application would become rather useless if there were no dependencies at all.
Which dependencies you need and in what direction they should point depends on the requirements. But because we’ve already established that we have no intention of replacing the domain layer with a completely different implementation, it’s safe to assume that other layers can safely depend on it. Figure 2.10 contains a big spoiler for the loosely coupled application you’ll write in the next chapter, but it does show the desired dependency graph.
Figure 2.10 Dependency graph of the desired situation
该图显示了我们如何反转域和数据访问层之间的依赖关系。我们将在下一章中详细介绍如何执行此操作。
The figure shows how we inverted the dependency between the domain and data access layers. We’ll go into more detail on how to do this in the next chapter.
2.3.3 杂项其他问题
2.3.3 Miscellaneous other issues
我们想指出一些应该解决的 Mary 代码的其他问题。
We’d like to point out a few other issues with Mary’s code that ought to be addressed.
Most of the domain model seems to be implemented in the data access layer. Whereas it’s a technical problem that the domain layer references the data access layer, it’s a conceptual problem that the data access layer defines such a class as the Product class. A public Product class belongs in the domain model.
On Jens’ advice, Mary decided to implement in the UI the code that determines whether a user is a preferred customer. But how a customer is identified as a preferred customer is a piece of business logic, so it should be implemented in the domain model. Jens’ argument about Separation of Concerns and the Single Responsibility Principle is no excuse for putting code in the wrong place. Following the Single Responsibility Principle within a single library is entirely possible — that’s the expected approach.
Mary 从CommerceContext类中的配置文件加载连接字符串(如清单 2.2所示)。从其消费者的角度来看,完全隐藏了对该配置值的依赖。正如我们在讨论清单 2.2时提到的,这种隐含包含一个陷阱。
Mary loaded the connection string from the configuration file from within theCommerceContextclass (shown in listing 2.2). From the perspective of its consumers, the dependency on this configuration value is completely hidden. As we alluded to when discussing listing 2.2, this implicitness contains a trap.
Although the ability to configure a compiled application is important, only the finished application should rely on configuration files. It’s more flexible for reusable libraries to be imperatively configurable by their callers, instead of reading configuration files themselves. In the end, the ultimate caller is the application’s entry point. At that point, all relevant configuration data can be read from a configuration file directly at startup and fed to the underlying libraries as needed. We want the configuration that CommerceContext requires to be explicit.
The view (as shown in listing 2.5) seems to contain too much functionality. It performs casts and specific string formatting. Such functionality should be moved to the underlying model.
2.4 结论
2.4 Conclusion
编写紧密耦合的代码非常容易。即使当 Mary 明确打算编写一个三层应用程序时,它也变成了一个大体上单一的意大利面条代码。5 (当我们谈论分层时,我们称之为烤宽面条。)
It’s surprisingly easy to write tightly coupled code. Even when Mary set out with the express intent of writing a three-layer application, it turned into a largely monolithic piece of Spaghetti Code.5 (When we’re talking about layering, we call this Lasagna.)
编写紧密耦合的代码如此容易的众多原因之一是语言特性和我们的工具已经把我们拉向了那个方向。如果你需要一个对象的新实例,你可以使用new关键字。如果您没有对所需程序集的引用,Visual Studio 可以轻松添加。但是每次你使用new关键字,你引入了一个紧耦合。正如第 1 章所讨论的,并不是所有的紧耦合都是不好的,但是你应该努力防止与易变依赖的紧耦合.
One of the many reasons that it’s so easy to write tightly coupled code is that both the language features and our tools already pull us in that direction. If you need a new instance of an object, you can use the new keyword. If you don’t have a reference to the required assembly, Visual Studio makes it easy to add. But every time you use the new keyword, you introduce a tight coupling. As discussed in chapter 1, not all tight coupling is bad, but you should strive to prevent tight coupling to Volatile Dependencies.
到现在为止,您应该开始理解是什么使紧密耦合的代码如此成问题,但我们还没有向您展示如何解决这些问题。在下一章中,我们将向您展示一种更具组合性的方法来构建具有与 Mary 构建的相同功能的应用程序。我们还将同时解决第 2.3.3 节中讨论的那些其他问题。
By now you should begin to understand what it is that makes tightly coupled code so problematic, but we’ve yet to show you how to fix these problems. In the next chapter, we’ll show you a more composable way of building an application with the same features as the one Mary built. We’ll also address those other issues discussed in section 2.3.3 at the same time.
概括
Summary
复杂的软件必须解决许多不同的问题,例如安全性、诊断、操作、性能和可扩展性。
Complex software must address lots of different concerns, such as security, diagnostics, operations, performance, and extensibility.
松散耦合鼓励您孤立地解决所有应用程序问题,但最终您仍必须组合这组复杂的问题。
Loose coupling encourages you to address all application concerns in isolation, but ultimately you must still compose this complex set of concerns.
It’s easy to create tightly coupled code. Although not all tight coupling is bad, tight coupling to Volatile Dependencies is and should be avoided.
在 Mary 的应用程序中,由于领域层依赖于数据访问层,因此无法用不同的数据访问层替换数据访问层。她的应用程序中的紧耦合导致 Mary 失去了松耦合提供的好处:后期绑定、可扩展性、可维护性、可测试性和并行开发。
In Mary’s application, because the domain layer depended on the data access layer, there was no way to replace the data access layer with a different one. The tight coupling in her application caused Mary to lose the benefits that loose coupling provides: late binding, extensibility, maintainability, Testability, and parallel development.
Only the finished application should rely on configuration files. Other parts of the application shouldn’t request values from a configuration file, but should instead be configurable by their callers.
单一职责原则指出每个类应该只有一个改变的理由。
The Single Responsibility Principle states that each class should only have one reason to change.
The Single Responsibility Principle can be viewed from the perspective of cohesion. Cohesion is defined as the functional relatedness of the elements of a class or module. The lower the amount of relatedness, the lower the cohesion; and the lower the cohesion, the greater the chance a class violates the Single Responsibility Principle.
3
编写松散耦合的代码
3
Writing loosely coupled code
在这一章当中
In this chapter
重新设计 Mary 的电子商务应用程序使其松散耦合
Redesigning Mary’s e-commerce application to become loosely coupled
When it comes to grilling steak, an important practice is to let the meat rest before you cut it into slices. When resting, the juices redistribute, and the results get juicier. If, on the other hand, you cut it too soon, all the juice runs out, and your meat gets drier and less tasty. It’d be a terrible shame to let this happen, because you’d like to give your guests the best tasting experience you can deliver. Although it’s important to know the best practices for any profession, it’s just as important to know the bad practices and to understand why those lead to unsatisfactory results.
Knowing the difference between good and bad practices is essential to learning. This is why the previous chapter was completely devoted to an example and analysis of tightly coupled code: the analysis provided you with the why.
总而言之,松散耦合提供了许多好处——后期绑定、可扩展性、可维护性、可测试性和并行开发。使用紧密耦合,您将失去这些好处。虽然并非所有的紧耦合都是不可取的,但你应努力避免与Volatile Dependencies的紧耦合。此外,您可以使用依赖注入 (DI) 来解决在分析过程中发现的问题。因为 DI 与 Mary 创建她的应用程序的方式截然不同,所以我们不打算修改她现有的代码。相反,我们将从头开始重新创建它。
To summarize, loose coupling provides a number of benefits — late binding, extensibility, maintainability, Testability, and parallel development. With tight coupling, you lose those benefits. Although not all tight coupling is undesirable, you should strive to avoid tight coupling to Volatile Dependencies. Moreover, you can use Dependency Injection (DI) to solve the issues that were discovered during that analysis. Because DI is a radical departure from the way Mary created her application, we’re not going to modify her existing code. Rather, we’re going to re-create it from scratch.
让我们首先简要回顾一下 Mary 的申请。我们还将讨论我们将如何处理重写以及完成后所需的结果。
Let’s start with a short recap of Mary’s application. We’ll also discuss how we’ll approach the rewrite and what the desired result will look like when we’ve finished.
3.1 重建电子商务应用
3.1 Rebuilding the e-commerce application
第2章分析Mary的应用,得出Volatile Dependencies在不同层之间紧密耦合。正如图 3.1中 Mary 的应用程序的依赖关系图所示,领域层和 UI 层都依赖于数据访问层。
The analysis of Mary’s application in chapter 2 concluded that Volatile Dependencies were tightly coupled across the different layers. As the dependency graph of Mary’s application in figure 3.1 shows, both the domain layer and the UI layer depend on the data access layer.
What we’ll aim to achieve in this chapter is to invert the dependency between the domain layer and the data access layer. This means that instead of the domain layer depending on the data access layer, the data access layer will depend on the domain layer, as shown in figure 3.2.
Figure 3.2 Dependency graph of the desired inversion for Mary’s application
通过创建此反转,我们允许替换数据访问层,而不必完全重写应用程序。(这与 Mary 开发她的应用程序的方式截然不同。)我们还将在此过程中应用几种模式。然后我们将应用构造函数注入,我们在第 1 章讨论过。最后,我们还将使用方法注入和Composition Root,我们将在进行时进行讨论。
By creating this inversion, we allow the data access layer to be replaced without having to completely rewrite the application. (This is a radical departure from the way Mary developed her application.) We’ll also apply several patterns along the way. Then we’ll apply Constructor Injection, which we discussed in chapter 1. And finally, we’ll also use Method Injection and Composition Root, which we’ll discuss as we go.
This approach will lead to quite a few more classes as we focus on separating the application concerns. Where Mary defined four classes, we’ll define nine classes and three interfaces. Figure 3.3 drills a little deeper into the application and shows the classes and interfaces we’ll create throughout this chapter.
Figure 3.4 shows how the main classes in the application will interact. At the end of this chapter, we’ll take a look at a slightly more detailed version of this diagram again.
Figure 3.4 Sequence diagram showing the interaction between elements involved in DI in the e-commerce application that we build in this chapter
当我们编写软件时,我们更喜欢从最重要的地方开始——利益相关者最能看到的部分。在 Mary 的电子商务应用程序中,这通常是 UI。从那里开始,我们开始工作,添加更多功能,直到完成一个功能;然后我们继续下一个。这种由外而内的技术帮助我们专注于所请求的功能,而不会过度设计解决方案。
When we write software, we prefer to start in the most significant place — the part that has most visibility to our stakeholders. As in Mary’s e-commerce application, this is often the UI. From there, we work our way in, adding more functionality until one feature is done; then we move on to the next. This outside-in technique helps us to focus on the requested functionality without overengineering the solution.
In chapter 2, Mary used the opposite approach. She started with the data access layer and worked her way out, working inside-out. It would be harsh for us to say that working inside-out is bad, but as you’ll see later, the outside-in approach gives you quicker feedback on what you’re building. We’ll therefore build the application in the opposite order, starting with the UI layer, continuing with the domain layer, and then building the data access layer last.
因为我们实践测试驱动开发(测试驱动开发),一旦我们的由外向内方法提示我们创建一个新类,我们就开始编写单元测试。尽管我们编写了单元测试来创建这个示例,但 TDD 并不是实现和使用 DI 所必需的,因此我们不会在本书中展示这些测试。如果您有兴趣,本书附带的源代码包括测试。让我们直接进入我们的项目并从 UI 开始。
Because we practice Test-Driven Development (TDD), we start by writing unit tests as soon as our outside-in approach prompts us to create a new class. Although we wrote unit tests to create this example, TDD isn’t required to implement and use DI, so we’re not going to show these tests in the book. If you’re interested, the source code that accompanies this book includes the tests. Let’s dive right into our project and begin with the UI.
3.1.1 构建更易于维护的 UI
3.1.1 Building a more maintainable UI
Mary 对特色产品列表的规范是编写一个应用程序,从数据库中提取这些项目并将它们显示在列表中(再次显示在图 3.5中)。因为我们知道项目利益相关者主要对视觉结果感兴趣,所以 UI 似乎是一个很好的起点。
Mary’s specification for the list of featured products was to write an application that extracts those items from the database and displays them in a list (shown again in figure 3.5). Because we know that the project stakeholders will mainly be interested in the visual result, the UI seems like a good place to start.
Figure 3.5 Screen capture of the e-commerce web application
打开 Visual Studio 后要做的第一件事是向解决方案中添加一个新的 ASP.NET Core MVC 应用程序。因为特色产品列表需要放在首页,所以您首先要修改 Index.cshtml 文件以包含以下清单中显示的标记。2个
The first thing you do after opening Visual Studio is add a new ASP.NET Core MVC application to the solution. Because the list of featured products needs to go on the front page, you start by modifying the Index.cshtml file to include the markup shown in the following listing.2
The first improvement is that you no longer cast a dictionary item to a sequence of products before iteration is possible. You accomplished this easily by using MVC’s special @model directive. This means that the Model property of the page is of the FeaturedProductsViewModel type. Using the @model directive, MVC will ensure that the value returned from the controller will be cast to the FeaturedProductsViewModel type. Secondly, the entire product display string is pulled directly from the SummaryText property of ProductViewModel.
Both improvements are related to the introduction of view-specific models that encapsulate the behavior of the view. These models are Plain Old CLR Objects (POCO).3 The following listing provides an outline of their structure.
Listing 3.3FeaturedProductsViewModel and ProductViewModel classes
public class FeaturedProductsViewModel
{
public FeaturedProductsViewModel(
IEnumerable<ProductViewModel> products)
{
this.Products = products;
}
public IEnumerable<ProductViewModel> Products ①
{ get; }
}
public class ProductViewModel
{
private static CultureInfo PriceCulture = new CultureInfo("en-US");
public ProductViewModel(string name, decimal unitPrice)
{
this.SummaryText = string.Format(PriceCulture,
"{0} ({1:C})", name, unitPrice);
}
public string SummaryText { get; } ②
}
视图模型的使用简化了视图,这很好,因为视图更难测试。它还使 UI 设计人员更容易处理应用程序。
The use of view models simplifies the view, which is good because views are harder to test. It also makes it easier for a UI designer to work on the application.
HomeController must return a view with an instance of FeaturedProductsViewModel for the code in listing 3.1 to work. As a first step, this can be implemented inside HomeController like this:
public ViewResult Index()
{
var vm = new FeaturedProductsViewModel(new[] ① { ① new ProductViewModel("Chocolate", 34.95m), ① new ProductViewModel("Asparagus", 39.80m) ① }); ① return this.View(vm); ②
}
我们在方法中硬编码了打折产品列表Index。这不是期望的最终结果,但它使 Web 应用程序能够无错误地执行,并允许我们向利益相关者展示一个不完整但正在运行的应用程序示例(存根),供他们评论。
We hard-coded the list of discounted products inside the Index method. This isn’t the desired end result, but it enables the web application to execute without error and allows us to show the stakeholders an incomplete, but running example of the application (a stub) for them to comment on.
Figure 3.6 Screen capture of the stubbed e-commerce web application. Here the product list is hard-coded.
在这个阶段,只实现了 UI 层的存根;域层和数据访问层的完整实现仍然存在。从 UI 开始的一个优势是我们已经拥有可以运行和测试的软件。将此与玛丽在可比阶段的进步进行对比。直到很晚的时候,Mary 才到达可以运行该应用程序的地步。图 3.6显示了存根的 Web 应用程序。
At this stage, only a stub of the UI layer has been implemented; a full implementation of the domain layer and data access layer still remains. One advantage of starting with the UI is that we already have software we can run and test. Contrast this with Mary’s progress at a comparable stage. Only at a much later stage does Mary arrive at a point where she can run the application. Figure 3.6 shows the stubbed web application.
For our HomeController to fulfill its obligations, and to do anything of interest, it requests a list of featured products from the domain layer. These products need to have discounts applied. In chapter 2, Mary wrapped this logic in her ProductService class, and we’ll do that too.
The Index method on HomeController should use the ProductService instance to retrieve the list of featured products, convert those to ProductViewModel instances, and then add those to FeaturedProductsViewModel. From the perspective of HomeController, however, ProductService is a Volatile Dependency because it’s a Dependency that doesn’t yet exist and is still in development. If we want to test HomeController in isolation, develop ProductService in parallel, or replace or Intercept it in the future, we need to introduce a Seam.
回想一下对 Mary 的实现的分析,依赖于易失性依赖是一个大罪。一旦这样做,您就会与刚刚使用的类型紧密耦合。为了避免这种紧密耦合,我们将引入一个接口并使用一种称为构造函数注入的技术;实例是如何创建的,由谁创建的,与HomeController.
Recall from the analysis of Mary’s implementation that depending on Volatile Dependencies is a cardinal sin. As soon as you do that, you’re tightly coupled with the type just used. To avoid this tight coupling, we’ll introduce an interface and use a technique called Constructor Injection; how the instance is created, and by whom, is of no concern to HomeController.
public class HomeController : Controller
{
private readonly IProductService productService;
public HomeController(
IProductService productService) ①
{
if (productService == null) ②
throw new ArgumentNullException(
"productService");
this.productService = productService; ③
}
public ViewResult Index()
{
IEnumerable<DiscountedProduct> products =
this.productService.GetFeaturedProducts(); ④ var vm = new FeaturedProductsViewModel( ⑤
from product in products
select new ProductViewModel(product)); ⑥
return this.View(vm);
}
}
As we stated in chapter 1, Constructor Injection is the act of statically defining the list of required Dependencies by specifying them as parameters to the class’s constructor. This is exactly what HomeController does. In its public constructor, it defines what Dependencies it requires for it to function correctly.
The first time we heard about Constructor Injection, we had a hard time understanding the real benefit. Doesn’t it push the burden of controlling the Dependency onto some other class? Yes, it does — and that’s the whole point. In an n-layer application, you can push that burden all the way to the top of the application into a Composition Root.
Because we added a constructor with an argument to HomeController, it’ll be impossible to create a HomeController without that Dependency, and that’s exactly why we did that. But that does mean that the application’s home screen is broken, because MVC has no idea how our HomeController must be created — unless you instruct MVC otherwise.
In fact, the creation of HomeController isn’t a concern of the UI layer; it’s the responsibility of the Composition Root.5 Because of this, we consider the UI layer completed, and we’ll come back to the creation of HomeController later on. Figure 3.7 shows the current state of implementing the architecture envisioned in figure 3.2.
Figure 3.7 At this stage, only the UI layer has been implemented; the domain and data access layers have yet to be addressed.
这将我们带到重新创建电子商务应用程序的下一阶段,即领域模型。
This leads us to the next stage in the re-creation of our e-commerce application, the domain model.
3.1.2 构建独立领域模型
3.1.2 Building an independent domain model
域模型是我们添加到解决方案中的普通 C# 库。该库将包含 POCO 和接口。POCO 将对域建模,而接口提供抽象,将作为我们进入域模型的主要外部入口点。他们将提供契约,域模型通过契约与即将到来的数据访问层进行交互。
The domain model is a plain, vanilla C# library that we add to the solution. This library will contain POCOs and interfaces. The POCOs will model the domain while the interfaces provide Abstractions that will serve as our main external entry points into the domain model. They’ll provide the contract through which the domain model interacts with the forthcoming data access layer.
上HomeController一节中交付的还没有编译,因为我们还没有定义IProductService抽象。在本节中,我们将向电子商务应用程序添加一个新的域层项目,并从 MVC 项目中引用域层项目,就像 Mary 所做的那样。结果没问题,但我们会推迟到 3.2 节再进行依赖图分析,以便我们可以为您提供全貌。以下清单显示了IProductService抽象。
The HomeController delivered in the previous section doesn’t compile yet because we haven’t defined the IProductServiceAbstraction. In this section, we’ll add a new domain layer project to the e-commerce application and a reference to the domain layer project from the MVC project, like Mary did. That will turn out OK, but we’ll postpone doing a dependency graph analysis until section 3.2 so that we can provide you with the full picture. The following listing shows the IProductServiceAbstraction.
IProductService represents the heart of our current domain layer in that it bridges the UI layer with the data access layer. It’s the glue that binds our initial application together.
IProductService抽象的唯一成员是GetFeaturedProducts方法. DiscountedProduct它返回实例的集合。每个DiscountedProduct包含一个Name和一个UnitPrice。这是一个简单的 POCO 类,如下一个清单所示,这个定义足以让我们编译我们的 Visual Studio 解决方案。
The sole member of the IProductServiceAbstraction is the GetFeaturedProducts method. It returns a collection of DiscountedProduct instances. Each DiscountedProduct contains a Name and a UnitPrice. It’s a simple POCO class, as can be seen in the next listing, and this definition gives us enough to compile our Visual Studio solution.
public class DiscountedProduct
{
public DiscountedProduct(string name, decimal unitPrice)
{
if (name == null) throw new ArgumentNullException("name");
this.Name = name;
this.UnitPrice = unitPrice;
}
public string Name { get; }
public decimal UnitPrice { get; }
}
针对接口而非具体类编程的原则是 DI 的基石。正是这一原则让您可以用一个具体实现替换另一个。在继续之前,我们应该花点时间了解一下接口在本次讨论中的作用。
The principle of programming to interfaces instead of concrete classes is a cornerstone of DI. It’s this principle that lets you replace one concrete implementation with another. Before continuing, we should take a quick moment to recognize the role of interfaces in this discussion.
Next we’ll write our ProductService implementation. The GetFeaturedProducts method of this ProductService class should use an IProductRepository instance to retrieve the list of featured products, apply any discounts, and return a list of DiscountedProduct instances.
A common abstraction over data access is provided by the Repository pattern, so we’ll define an IProductRepository abstraction in the domain model library.6
IProductRepository is the interface to the data access layer, returning “raw” Entities from the persistence store. By contrast, IProductService applies business logic, such as the discount in this case, and converts the Entities to a narrower-focused object. A full-blown Repository would have more methods to find and modify products, but, following the outside-in principle, we only define the classes and members needed for the task at hand. It’s easier to add functionality to code than it is to remove anything.
Because our goal is to invert the dependency between the domain layer and the data access layer, IProductRepository is defined in the domain layer. In the next section, we’ll create an implementation of IProductRepository as part of the data access layer. This allows our dependency to point at the domain layer.
Product班级_也是用最少的成员实现的,如以下清单所示。
The Product class is also implemented with the bare minimum of members, as shown in the following listing.
public class Product
{
public string Name { get; set; } ① public decimal UnitPrice { get; set; } ① public bool IsFeatured { get; set; } ①
public DiscountedProduct ApplyDiscountFor(
IUserContext user) ②
{
bool preferred = ③ user.IsInRole(Role.PreferredCustomer); ③ ③ decimal discount = preferred ? .95m : 1.00m; ③ ③ return new DiscountedProduct( ③ name: this.Name, ③ unitPrice: this.UnitPrice * discount); ③
}
}
GetFeaturedProducts方法_类的ProductService应该使用一个IProductRepository实例来检索特色产品列表,应用任何折扣,并返回一个DiscountedProduct实例列表。ProductService班级_对应于 Mary 的同名类,但现在是纯域模型类,因为它没有对数据访问层的硬编码引用。对于我们的HomeController,我们将再次使用构造函数注入放弃对其易失性依赖项的控制,如下所示。
The GetFeaturedProducts method of the ProductService class should use an IProductRepository instance to retrieve the list of featured products, apply any discounts, and return a list of DiscountedProduct instances. The ProductService class corresponds to Mary’s class of the same name, but is now a pure domain model class because it doesn’t have a hard-coded reference to the data access layer. As with our HomeController, we’re again going to relinquish control of its Volatile Dependencies using Constructor Injection, as shown next.
Listing 3.9ProductService with Constructor Injection
public class ProductService : IProductService
{
private readonly IProductRepository repository;
private readonly IUserContext userContext;
public ProductService(
IProductRepository repository, ① IUserContext userContext) ①
{
if (repository == null)
throw new ArgumentNullException("repository");
if (userContext == null)
throw new ArgumentNullException("userContext");
this.repository = repository;
this.userContext = userContext;
}
public IEnumerable<DiscountedProduct> GetFeaturedProducts()
{
return
from product in this.repository ② .GetFeaturedProducts() ② select product ② .ApplyDiscountFor(this.userContext); ③
}
}
Besides an IProductRepository, the ProductService constructor requires an instance of IUserContext:
public interface IUserContext
{
bool IsInRole(Role role);
}
public enum Role { PreferredCustomer }
这是与 Mary 的实现的另一个不同之处,后者仅将布尔值作为GetFeaturedProducts方法的参数,指示用户是否是首选客户。因为决定用户是否是首选客户是域层的一部分,所以将其显式建模为Dependency更为正确。除此之外,有关代表其运行请求的用户的信息是上下文相关的。我们不希望每个控制器都负责收集这些信息。这将是重复的并且容易出错,并且可能导致意外的安全漏洞。
This is another departure from Mary’s implementation, which only took a boolean value as argument to the GetFeaturedProducts method, indicating whether the user is a preferred customer. Because deciding whether a user is a preferred customer is a piece of the domain layer, it’s more correct to explicitly model this as a Dependency. Besides that, information about the user on whose behalf the request is running is contextual. We don’t want every controller to be responsible for gathering this information. That would be repetitive and error prone, and might lead to accidental security bugs.
Instead of letting the UI layer provide this information to the domain layer, we allow the retrieval of this information to become an implementation detail of ProductService. The IUserContext interface allows ProductService to retrieve information about the current user without HomeController needing to provide this. HomeController doesn’t need to know which role(s) are authorized for a discount price, nor is it easy for HomeController to inadvertently enable the discount by passing, for example, true instead of false. This reduces the overall complexity of the UI layer.
Although the .NET Base Class Library (BCL) includes an IPrincipal interface, which represents a standard way of modeling application users, that interface is generic in nature and isn’t tailored for our application’s special needs. Instead, we let the application define the Abstraction.
The ProductService.GetFeaturedProducts method passes the IUserContextDependency on to the Product.ApplyDiscountFor method. This technique is known as Method Injection. Method Injection is particularly useful in cases where short-lived objects like Entities (such as the ProductEntity, in our case) need Dependencies. Although the details vary, the main technique remains the same. We’ll discuss this pattern in more detail in chapter 4. At this stage, the application doesn’t work at all. That’s because three problems remain:
There’s no concrete implementation of IProductRepository. This is easily solved. In the next section, we’ll implement a concrete SqlProductRepository that reads the featured products from the database.
没有具体的实现IUserContext。我们也将在下一节中对此进行介绍。
There’s no concrete implementation ofIUserContext. We’ll take a look at this in the next section too.
The MVC framework doesn’t know which concrete type to use. This is because we introduced an abstract parameter of type IProductService to the constructor of HomeController. This issue can be solved in various ways, but our preference is to develop a custom Microsoft.AspNetCore.Mvc.Controllers.IControllerActivator. How this is done is outside the scope of this chapter, but it’s a subject that we’ll discuss in chapter 7. Suffice it to say that this custom factory will create an instance of the concrete ProductService and supply it to the constructor of HomeController.
In the domain layer, we work only with types defined within the domain layer and Stable Dependencies of the .NET BCL. The concepts of the domain layer are implemented as POCOs. At this stage, there’s only a single concept represented, namely, a Product. The domain layer must be able to communicate with the outside world (such as databases). This need is modeled as Abstractions (such as Repositories) that we must replace with concrete implementations before the domain layer becomes useful. Figure 3.9 shows the current state of implementing the architecture envisioned in figure 3.2.
We succeeded in making our domain model compile. This means that we created a domain model that’s independent of the data access layer, which we still need to create. But before we get to that, there are a few points we’d like to explain in more detail.
依赖倒置原则
Dependency Inversion Principle
我们试图用 DI 完成的大部分工作都与依赖倒置原则有关。8 该原则指出,我们应用程序中的高层模块不应依赖于低层模块;相反,两个级别的模块都应该依赖于Abstractions。
Much of what we’re trying to accomplish with DI is related to the Dependency Inversion Principle.8 This principle states that higher-level modules in our applications shouldn’t depend on lower-level modules; instead, modules of both levels should depend on Abstractions.
This is exactly what we did when we defined our IProductRepository. The ProductService component is part of the higher-level domain layer module, whereas the IProductRepository implementation — let’s call it SqlProductRepository — is part of the lower-level data access module. Instead of letting our ProductService depend on SqlProductRepository, we let both ProductService and SqlProductRepository depend on the IProductRepository Abstraction. SqlProductRepository implements the Abstraction, while ProductService uses it. Figure 3.10 illustrates this.
Dependency Inversion Principle和 DI 的关系是,Dependency Inversion Principle规定了我们想要完成什么,DI 说明了我们想要如何完成。该原则并未描述消费者应如何获取其Dependencies。然而,许多开发人员并没有意识到依赖倒置原则的另一个有趣的部分。
The relationship between the Dependency Inversion Principle and DI is that the Dependency Inversion Principle prescribes what we would like to accomplish, and DI states how we would like to accomplish it. The principle doesn’t describe how a consumer should get ahold of its Dependencies. Many developers, however, aren’t aware of another interesting part of the Dependency Inversion Principle.
Not only does the principle prescribe loose coupling, it states that Abstractions should be owned by the module using the Abstraction. In this context, “owned” means that the consuming module has control over the shape of the Abstraction, and it’s distributed with that module, rather than with the module that implements it. The consuming module should be able to define the Abstraction in a way that benefits itself the most.
You already saw us do this twice: both IUserContext and IProductRepository are defined this way. They’re designed in a way that works best for the domain layer, even though their implementations are the responsibility of the UI and data access layers, respectively, as shown in figure 3.11.
Letting a higher-level module or layer define its own Abstractions not only prevents it from having to take a dependency on a lower-level module, it allows the higher-level module to be simplified, because the Abstractions are tailored for its specific needs. This brings us back to the BCL’s IPrincipal interface.
As we described, IPrincipal is generic in nature. The Dependency Inversion Principle instead guides us towards defining Abstractions tailored for our application’s special needs. That’s why we define our own IUserContextAbstraction instead of letting the domain layer depend on IPrincipal. This does mean, however, that we have to create an Adapter implementation that allows translating calls from this application-specific IUserContextAbstraction to calls to the application framework.
If the Dependency Inversion Principle dictates that Abstractions should be distributed with their owning modules, doesn’t the domain layer IProductService interface violate this principle? After all, IProductService is consumed by the UI layer, but implemented by the domain layer, as figure 3.12 shows. The answer is yes, this does violate the Dependency Inversion Principle.
If we were keen on fixing this violation, we should move IProductService out of the domain layer. Moving IProductService into the UI layer, however, would make our domain layer dependent on that layer. Because the domain layer is the central part of the application, we don’t want it to depend on anything else. Besides, this dependency would make it impossible to replace the UI later on.
This means that to fix the violation, we need an additional two extra projects in our solution — one for the isolated UI layer without the Composition Root and another for the IProductServiceAbstraction that the UI layer owns. Out of pragmatism, however, we chose not to pursuit this path for this example and, therefore, leave the violation in place. We hope you can appreciate that we don’t want to overcomplicate things.
Many guides to object-oriented design focus on interfaces as the main abstraction mechanism, whereas the .NET Framework Design Guidelines endorse abstract classes over interfaces.9 Should you use interfaces or abstract classes? With relation to DI, the reassuring answer is that it doesn’t matter. The important part is that you program against some sort of abstraction.
Choosing between interfaces and abstract classes is important in other contexts, but not here. You’ll notice that we use these words interchangeably; we often use the term Abstraction to encompass both interfaces and abstract classes. This doesn’t mean that we, as authors, don’t have a preference for one over the other. We do, in fact. When it comes to writing applications, we typically prefer interfaces over abstract classes for these reasons:
Abstract classes can easily be abused as base classes. Base classes can easily turn into ever-changing, ever-growing God Objects.10 The derivatives are tightly coupled to its base class, which can become a problem when the base class contains Volatile behavior. Interfaces, on the other hand, force us into the “Composition over Inheritance” mantra.11
Concrete classes can implement several interfaces, although in .NET, those can only derive from a single base class. Using interfaces as the vehicle of Abstraction is more flexible.
Interface definitions in C# are less clumsy compared to abstract classes. With interfaces, we can omit the abstract and public keywords from their members. This makes an interface a more succinct definition.
When writing reusable libraries, however, the subject is becoming less clear-cut, due to the need to deal with backward compatibility. In that light, an abstract class might make more sense because non-abstract members can be added later, whereas adding members to an interface is a breaking change. That’s why the .NET Framework Design Guidelines prefer abstract classes.
现在让我们转到数据访问层。我们将为先前定义的IProductRepository接口创建一个实现。
Now let’s move on to the data access layer. We’ll create an implementation for the previously defined IProductRepository interface.
3.1.3 构建新的数据访问层
3.1.3 Building a new data access layer
像 Mary 一样,我们希望使用 Entity Framework Core 来实现我们的数据访问层,因此我们按照她在第 2 章中执行的相同步骤来创建实体模型。主要区别在于现在只是数据访问层的一个实现细节,而不是整个数据访问层。CommerceContext
Like Mary, we’d like to implement our data access layer using Entity Framework Core, so we follow the same steps she did in chapter 2 to create the Entity model. The main difference is that CommerceContext is now only an implementation detail of the data access layer, as opposed to being the entirety of the data access layer.
In this model, nothing outside of the data access layer will have any awareness of, or dependency on, Entity Framework. It can be swapped out without any upstream effects. With that in mind, we can create an implementation of IProductRepository.
Listing 3.10 Implementing IProductRepository using Entity Framework Core
public class SqlProductRepository : IProductRepository
{
private readonly CommerceContext context;
public SqlProductRepository(CommerceContext context)
{
if (context == null) throw new ArgumentNullException("context");
this.context = context;
}
public IEnumerable<Product> GetFeaturedProducts()
{
return
from product in this.context.Products
where product.IsFeatured
select product;
}
}
在 Mary 的应用程序中,Product实体也被用作域对象,尽管它是在数据访问层中定义的。这已不再是这种情况。该类Product现在已在我们的领域层中定义。我们的数据访问层重用了Product类从那层。
In Mary’s application, the ProductEntity was also used as a domain object, although it was defined in the data access layer. This is no longer the case. The Product class is now defined in our domain layer. Our data access layer reuses the Product class from that layer.
For simplicity, we chose to let the data access layer reuse our domain object instead of defining its own implementation. We were able to do so because Entity Framework Core allows us to write Entities that are persistence ignorant.12 Whether this is a reasonable practice depends a lot on the structure and complexity of your domain objects. If we later conclude that this shared model is enforcing unwanted constraints on our model, we can change our data access layer by introducing internal persistence objects, without touching the rest of the application. In that case, we’d need the data access layer to convert those internal persistence objects into domain objects.
在上一章中,我们讨论了 MaryCommerceContext对连接字符串的隐式依赖是如何导致她一路走来的问题的。我们的 newCommerceContext将使这种依赖关系显式化,这是与 Mary 的实现的另一个偏差。下一个清单显示了我们的新CommerceContext.
In the previous chapter, we discussed how the implicit dependency of Mary’s CommerceContext on the connection string caused her problems along the way. Our new CommerceContext will make this dependency explicit, which is another deviation from Mary’s implementation. The next listing shows our new CommerceContext.
public class CommerceContext : DbContext
{
private readonly string connectionString;
public CommerceContext(string connectionString) ①
{
if (string.IsNullOrWhiteSpace(connectionString))
throw new ArgumentException(
"connectionString should not be empty.",
"connectionString");
this.connectionString = connectionString; ②
}
public DbSet<Product> Products { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
builder.UseSqlServer(this.connectionString);
}
}
这几乎让我们结束了电子商务应用程序的重新实现。唯一仍然缺少的实现是IUserContext.
This almost brings us to the end of our re-implementation of the e-commerce application. The only implementation still missing is that of IUserContext.
3.1.4 实现特定于 ASP.NET Core 的IUserContext适配器
3.1.4 Implementing an ASP.NET Core–specific IUserContext Adapter
缺少的最后一个具体实现是. 在 Web 应用程序中,有关发出请求的用户的信息通常会随每个请求一起传递到服务器。此信息使用 cookie 或 HTTP 标头进行中继。我们如何检索当前用户的身份在很大程度上取决于我们使用的框架。这意味着我们在构建 ASP.NET Core 应用程序时需要完全不同的实现,例如与 Windows 服务相比。IUserContext
The last concrete implementation missing is that of IUserContext. In web applications, information about a user who issues a request is usually passed on to the server with each request. This information is relayed using cookies or HTTP headers. How we retrieve the identity of the current user is highly dependent on the framework we use. This means that we’ll need a completely different implementation when building an ASP.NET Core application compared with, for instance, a Windows service.
The implementation of our IUserContext is framework specific. We want neither our domain layer nor our data layer to know anything about the application framework. That would make it impossible to use those layers in a different context. We need to implement this elsewhere. The UI layer, therefore, is an ideal place for our IUserContext implementation.
以下清单显示了IUserContextASP.NET Core 应用程序的可能实现。
The following listing shows a possible IUserContext implementation for an ASP.NET Core application.
Listing 3.12IUserContext implementation for ASP.NET Core
public class AspNetUserContextAdapter : IUserContext
{
private static HttpContextAccessor Accessor = new HttpContextAccessor();
public bool IsInRole(Role role)
{
return Accessor.HttpContext.User.IsInRole(role.ToString());
}
}
AspNetUserContextAdapter需要一个工作。,一个由 ASP.NET Core 框架指定的组件,允许访问当前请求的 ,就像我们能够在 ASP.NET “经典”中使用. 我们用来访问有关当前用户的请求信息。HttpContextAccessorHttpContextAccessorHttpContextHttpContext.CurrentHttpContext
AspNetUserContextAdapter requires an HttpContextAccessor to work. HttpContextAccessor, a component specified by the ASP.NET Core framework, allows access to the HttpContext of the current request, like we were able to in ASP.NET “classic” using HttpContext.Current. We use HttpContext to access the request’s information about the current user.
AspNetUserContextAdapter adapts our application-specific IUserContextAbstraction to the ASP.NET Core API. This class is an implementation of the Adapter design pattern that we discussed in chapter 1.13
With AspNetUserContextAdapter implemented, our reimplementation of the e-commerce application is finished. This brings us to our Composition Root.
3.1.5 在Composition Root中组合应用
3.1.5 Composing the application in the Composition Root
有了,和实现后,我们现在可以设置 ASP.NET Core MVC 来构建 的实例,其中由一个实例提供,该实例本身是使用一个和一个. 这最终会产生如下所示的对象图。ProductServiceSqlProductRepositoryAspNetUserContextAdapterHomeControllerHomeControllerProductServiceSqlProductRepositoryAspNetUserContextAdapter
With ProductService, SqlProductRepository and AspNetUserContextAdapter implemented, we can now set up ASP.NET Core MVC to construct an instance of HomeController, where HomeController is fed by a ProductService instance, which itself is constructed using a SqlProductRepository and an AspNetUserContextAdapter. This eventually results in an object graph that would look as follows.
Figure 3.13 Screen capture of the finished application
我们将在第 7 章中更详细地讨论如何将此类对象图的构造插入到 ASP.NET Core 框架中,因此我们不会在这里展示。但现在一切都正确连接在一起,我们可以浏览到应用程序的主页并获得如图 3.13所示的页面。
We’ll discuss how the construction of such an object graph is plugged into the ASP.NET Core framework in greater detail in chapter 7, so we won’t show that here. But now that everything is correctly wired together, we can browse to the application’s homepage and get the page shown in figure 3.13.
The previous section contained lots of details, so it’s hardly surprising if you lost sight of the big picture along the way. In this section, we’ll try to explain what happened in broader terms.
3.2.1 理解组件之间的交互
3.2.1 Understanding the interaction between components
The classes in each layer interact with each other either directly or in abstract form. They do so across module boundaries, so it can be difficult to follow how they interact. Figure 3.14 shows how the different Dependencies interact, giving a more detailed overview to the original outline described in figure 3.4.
When the application starts, the code in Startup creates a new custom controller activator and looks up the connection string from the application’s configuration file. When a page request comes in, the application invokes Create on the controller activator.
The activator supplies the stored connection string to a new instance of CommerceContext (not shown in the diagram). It injects CommerceContext into a new instance of SqlProductRepository. In turn, the SqlProductRepository instance together with an instance of AspNetUserContextAdapter (not shown in the diagram) are injected into a new instance of ProductService. Similarly, ProductService is injected into a new instance of HomeController, which is then returned from the Create method.
然后 ASP.NET Core MVC 框架调用实例Index上的方法HomeController,使其调用实例GetFeaturedProducts上的方法。这又会调用实例上的方法。最后,填充后的 返回,MVC 找到并呈现正确的视图。ProductRepositoryGetFeaturedProductsSqlProductRepositoryViewResultFeaturedProductsViewModel
The ASP.NET Core MVC framework then invokes the Index method on the HomeController instance, causing it to invoke the GetFeaturedProducts method on the ProductRepository instance. This in turn calls the GetFeaturedProducts method on the SqlProductRepository instance. Finally, the ViewResult with the populated FeaturedProductsViewModel is returned, and MVC finds and renders the correct view.
In section 2.2, you saw how a dependency graph can help you analyze and understand the degree of flexibility provided by the architectural implementation. Has DI changed the dependency graph for the application?
Figure 3.15 shows that the dependency graph has indeed changed. The domain model no longer has any dependencies and can act as a standalone module. On the other hand, the data access layer now has a dependency; in Mary’s application, it had none.
图 3.15 显示应用了 DI 的示例电子商务应用程序的依赖关系图。显示了所有类和接口,以及它们之间的关系。
Figure 3.15 Dependency graph showing the sample e-commerce application with DI applied. All classes and interfaces are shown, as well as their relationships to one another.
The most important thing to note in figure 3.15 is that the domain layer no longer has any dependencies. This should raise our hopes that we can answer the original questions about composability (see section 2.2) more favorably this time:
我们可以更换基于网络的用户界面吗使用基于 WPF 的 UI?这在以前是可能的,并且在新设计中仍然是可能的。域模型库和数据访问库都不依赖于基于 Web 的 UI,因此我们可以轻松地在其位置放置其他内容。
Can we replace the web-based UI with a WPF-based UI? That was possible before and is still possible with the new design. Neither the domain model library nor the data access library depends on the web-based UI, so we can easily put something else in its place.
Can we replace the relational data access layer with one that works with the Azure Table Service? In a later chapter, we’ll describe how the application locates and instantiates the correct IProductRepository, so, for now, take the following at face value: the data access layer is being loaded by late binding, and the type name is defined as an application setting in the application’s configuration file. It’s possible to throw the current data access layer away and inject a new one, as long as it also provides an implementation of IProductRepository.
本章中描述的示例电子商务应用程序只向我们展示了有限的复杂程度:只读场景中只涉及一个存储库。到目前为止,我们一直保持应用程序尽可能简单和小巧,以温和地介绍一些核心概念和原则。因为 DI 的主要目的之一是管理复杂性,所以我们需要一个复杂的应用程序来充分领会它的力量。在本书的学习过程中,我们将扩展示例电子商务应用程序以充分展示 DI 的不同方面。
The sample e-commerce application described in this chapter only presents us with a limited level of complexity: there’s only a single Repository involved in a read-only scenario. Until now, we’ve kept the application as simple and small as possible to gently introduce some core concepts and principles. Because one of the main purposes of DI is to manage complexity, we need a complex application to fully appreciate its power. During the course of the book, we’ll expand the sample e-commerce application to fully demonstrate different aspects of DI.
本章结束了本书的第一部分。第 1 部分的目的是让 DI 广为人知,并从总体上介绍 DI。在本章中,您看到了构造函数注入的示例。我们还介绍了Method Injection和Composition Root作为与 DI 相关的模式。在下一章中,我们将深入探讨这些和其他设计模式。
This chapter concludes the first part of the book. The purpose of part 1 was to put DI on the map and to introduce DI in general. In this chapter, you’ve seen examples of Constructor Injection. We also introduced Method Injection and Composition Root as patterns related to DI. In the next chapter, we’ll dive deeper into these and other design patterns.
Refactoring existing applications towards a more maintainable, loosely coupled design is hard. Big rewrites, on the other hand, are often riskier and expensive.
使用视图模型可以简化视图,因为传入的数据是专门为视图设计的。
The use of view models can simplify the view, because the incoming data is shaped specifically for the view.
因为视图更难测试,所以视图越笨越好。它还简化了可能处理视图的 UI 设计人员的工作。
Because views are harder to test, the dumber the view, the better. It also simplifies the work of a UI designer who might work on the view.
当您限制域层内易失性依赖项的数量时,您将获得更高程度的解耦、重用和可测试性。
When you limit the amount of Volatile Dependencies within the domain layer, you get a higher degree of decoupling, reuse, and Testability.
在构建应用程序时,由外而内的方法有助于更快速地制作原型,从而缩短反馈周期。
When building applications, the outside-in approach facilitates more rapid prototyping, which can shorten the feedback cycle.
When you want a high degree of modularity in your application, you need to apply the Constructor Injection pattern and build object graphs in the Composition Root, which is located close to the application’s entry point.
接口编程是 DI 的基石。它允许您替换、模拟和拦截依赖项,而无需对其使用者进行更改。当实现和抽象放在不同的程序集中时,它可以替换整个库。
Programming to interfaces is a cornerstone of DI. It allows you to replace, mock, and Intercept a Dependency, without having to make changes to its consumers. When implementation and Abstraction are placed in different assemblies, it enables whole libraries to be replaced.
Programming to interfaces doesn’t mean that all classes should implement an interface. Short-lived objects, such as Entities, view models, and DTOs, typically contain no behavior that requires mocking, Interception, decoration, or replacement.
With respect to DI, it doesn’t matter whether you use interfaces or purely abstract classes. From a general development perspective, as authors, we typically prefer interfaces over abstract classes.
A reusable library is a library that has clients that aren’t known at compile time. Reusable libraries are typically shipped via NuGet. Libraries that only have callers within the same (Visual Studio) solution aren’t considered to be reusable libraries.
DI 与依赖倒置原则密切相关。这个原则意味着你应该针对接口进行编程,并且一个层必须控制它使用的接口。
DI is closely related to the Dependency Inversion Principle. This principle implies that you should program against interfaces, and that a layer must be in control over the interfaces it uses.
使用DI 容器有助于使应用程序的组合根更易于维护,但它不会神奇地使紧密耦合的代码松散耦合。为了使应用程序变得可维护,必须在设计时考虑到 DI 模式和技术。
The use of a DI Container can help in making the application’s Composition Root more maintainable, but it won’t magically make tightly coupled code loosely coupled. For an application to become maintainable, it must be designed with DI patterns and techniques in mind.
第 2 部分
目录
Part 2
Catalog
P第 1 条概述了 DI,讨论了 DI 的目的和好处。尽管第 3 章包含了一个广泛的示例,但我们确信第一章仍然给您留下了一些未解决的问题。在第 2 部分中,我们将更深入地挖掘以回答其中的一些问题。
Part 1 provided an overview of DI, discussing the purpose and benefits of DI. Even though chapter 3 contained an extensive example, we’re sure the first chapters still left you with some unresolved questions. In part 2, we’ll dig a little deeper to answer some of those questions.
正如标题所暗示的,第 2 部分提供了模式、反模式和代码味道的完整目录。有些人不喜欢设计模式,因为他们觉得它们枯燥或过于抽象。就个人而言,我们喜欢模式,因为它们为我们提供了一种高级语言,使我们在讨论软件设计时更加高效和简洁。我们的目的是使用这个目录为 DI 提供一种模式语言。尽管模式描述必须包含一些概括,但我们使用示例使每个模式具体化。您可以按顺序阅读所有三章,但目录中的每个项目也都已写好,以便您可以单独阅读。
As the title implies, part 2 presents a complete catalog of patterns, anti-patterns, and code smells. Some people dislike design patterns, because they find them dry or too abstract. Personally, we love patterns, because they provide us with a high-level language that makes us more efficient and concise when we discuss software design. It’s our intent to use this catalog to provide a pattern language for DI. Although a pattern description must contain some generalizations, we’ve made each pattern concrete, using examples. You can read all three chapters in sequence, but each item in the catalog is also written so that you can read it by itself.
第 4 章包含 DI 设计模式的迷你目录。从某种意义上说,这些模式构成了有关如何实施 DI 的规范性指导,但您应该知道,我们并不认为它们具有同等重要性。Constructor Injection和Composition Root是迄今为止最重要的设计模式,而所有其他模式应视为可以在特殊情况下应用的边缘案例。
Chapter 4 contains a mini catalog of DI design patterns. In a sense, these patterns constitute prescriptive guidance on how to implement DI, but you should be aware that we don’t consider them to be of equal importance. Constructor Injection and Composition Root are by far the most important design patterns, whereas all the other patterns should be treated as fringe cases that can be applied in specialized circumstances.
第 4 章为您提供了一组通用的解决方案,而第 5 章包含了一系列应避免的情况。这些反模式描述了解决典型 DI 挑战的常见但不正确的方法。在每种情况下,反模式都描述了如何识别事件以及如何解决问题。了解和理解这些反模式对于避免它们所代表的陷阱非常重要,而且正如第 4 章介绍的两个最重要的模式一样,最重要的反模式是Service Locator,它是 DI 的对立面。
Whereas chapter 4 gives you a set of generalized solutions, chapter 5 contains a catalog of situations to avoid. These anti-patterns describe common, but incorrect ways to address typical DI challenges. In each case, the anti-pattern describes how to identify occurrences and how to resolve the issue. It’s important to know and understand these anti-patterns to avoid the traps that they represent, and, just as chapter 4 presents two dominatingly important patterns, the most important anti-pattern is Service Locator, the antithesis of DI.
当您将 DI 应用于现实生活中的编程任务时,您会遇到一些挑战。我们认为我们都曾有过怀疑自己是否理解某个工具的时刻或技术,但我们认为,“理论上,这可能有效,但我的情况很特殊。” 当我们发现自己有这样的想法时,我们很清楚我们还有更多东西要学。
As you apply DI to real-life programming tasks, you’ll run into some challenges. We think we’ve all had moments of doubt where we feel that we understand a tool or technique, and yet we think, “In theory, this may work, but my case is special.” When we find ourself thinking like this, it’s clear to us that we have more to learn.
在我们的职业生涯中,我们看到了一组特定的问题一再出现。这些问题中的每一个都有一个通用的解决方案,您可以将您的代码应用到第 4 章中的一种 DI 模式。第 6 章包含这些常见问题或代码异味及其相应解决方案的目录。
During our career, we’ve seen a particular set of problems appear again and again. Each of these problems has a general solution you can apply to move your code towards one of the DI patterns from chapter 4. Chapter 6 contains a catalog of these common problems, or code smells, and their corresponding solutions.
We expect this to be the most useful part of the book, because it’s the most enduring. Hopefully, you’ll return to these chapters months and even years after you first read them.
4
种DI模式
4
DI patterns
在这一章当中
In this chapter
使用Composition Root组合对象图
Composing object graphs with Composition Root
使用构造函数注入静态声明所需的依赖项
Statically declaring required Dependencies with Constructor Injection
使用方法注入将依赖项传递到组合根之外
Passing Dependencies outside the Composition Root with Method Injection
使用属性注入声明可选依赖项
Declaring optional Dependencies with Property Injection
Like all professionals, cooks have their own jargon that enables them to communicate about complex food preparation in a language that often sounds esoteric to the rest of us. It doesn’t help that most of the terms they use are based on the French language (unless you already speak French, that is). Sauces are a great example of the way cooks use their professional terminology. In chapter 1, we briefly discussed sauce béarnaise, but we didn’t elaborate on the taxonomy that surrounds it.
A sauce béarnaise is really a sauce hollandaise where the lemon juice is replaced by a reduction of vinegar, shallots, chervil, and tarragon. Other sauces are based on sauce hollandaise, including Mark’s favorite, sauce mousseline, which is made by folding whipped cream into the hollandaise.
Did you notice the jargon? Instead of saying, “carefully mix the whipped cream into the sauce, taking care not to collapse it,” we used the term folding. Instead of saying, “thickening and intensifying the flavor of vinegar,” we used the term reduction. Jargon allows you to communicate concisely and effectively.
In software development, we have a complex and impenetrable jargon of our own. You may not know what the cooking term bain-marie refers to, but we’re pretty sure most chefs would be utterly lost if you told them that “strings are immutable classes, which represent sequences of Unicode characters.” And when it comes to talking about how to structure code to solve particular types of problems, we have design patterns that give names to common solutions. In the same way that the terms sauce hollandaise and fold help us succinctly communicate how to make sauce mousseline, design patterns help us talk about how code is structured.
We’ve already named quite a few software design patterns in the previous chapters. For instance, in chapter 1 we talked about the patterns Abstract Factory, Null Object, Decorator, Composite, Adapter, Guard Clause, Stub, Mock, and Fake. Although, at this point, you might not be able to recall each of them, you probably won’t feel that uncomfortable if we talk about design patterns. We human beings like to name reoccurring patterns, even if they’re simple.
如果您对设计模式的一般了解有限,请不要担心。设计模式的主要目的是提供对实现目标的特定方式的详细且独立的描述——如果您愿意的话,也可以是配方。此外,您已经看到了我们将在本章中描述的四种基本 DI 设计模式中的三种示例:
Don’t worry if you have only a limited knowledge of design patterns in general. The main purpose of a design pattern is to provide a detailed and self-contained description of a particular way of attaining a goal — a recipe, if you will. And besides, you already saw examples of three out of the four basic DI design patterns that we’ll describe in this chapter:
Composition Root——描述你应该在哪里以及如何组合应用程序的对象图。
Composition Root — Describes where and how you should compose an application’s object graphs.
构造函数注入——允许类静态声明其所需的依赖项。
Constructor Injection — Allows a class to statically declare its required Dependencies.
方法注入——当依赖项或消费者可能因每个操作而改变,使您能够向消费者提供依赖项。
Method Injection — Enables you to provide a Dependency to a consumer when either the Dependency or the consumer might change for each operation.
Property Injection — Allows clients to optionally override some class’s default behavior, where this default behavior is implemented in a Local Default.
This chapter is structured to provide a catalog of patterns. For each pattern, we’ll provide a short description, a code example, advantages and disadvantages, and so on. You can read about all four patterns introduced in this chapter in sequence or only read the ones that interest you. The most important patterns are Composition Root and Constructor Injection, which you should use in most situations — the other patterns become more specialized as the chapter progresses.
4.1 组合根
4.1 Composition Root
我们应该在哪里编写对象图?
Where should we compose object graphs?
尽可能靠近应用程序的入口点。
As close as possible to the application’s entry point.
When you’re creating an application from many loosely coupled classes, the composition should take place as close to the application’s entry point as possible. The Main method is the entry point for most application types. The Composition Root composes the object graph, which subsequently performs the actual work of the application.
Figure 4.1 Close to the application’s entry point, the Composition Root takes care of composing object graphs of loosely coupled classes. The Composition Root takes a direct dependency on all modules in the system.
In the previous chapter, you saw that most classes used Constructor Injection. By doing so, they pushed the responsibility for the creation of their Dependencies up to their consumers. Such consumers, however, also pushed the responsibility for creating their Dependencies up to their consumers.
You can’t delay the creation of your objects indefinitely. There must be a location where you create your object graphs. You should concentrate this creation into a single area of your application. This place is called the Composition Root.
In the previous chapter, this resulted in the object graph that you saw in listing 3.13 (figure 4.1). This listing also shows that all components from all application layers are constructed in the Composition Root.
Listing 4.1 The application’s object graph from chapter 3
new HomeController( ① new ProductService( ② new SqlProductRepository( ③ new CommerceContext(connectionString)), ③ new AspNetUserContextAdapter())); ④
如果您有一个控制台应用程序是为在这个特定的对象图上运行而编写的,它可能看起来如下面的清单所示。
If you were to have a console application that was written to operate on this particular object graph, it might look as shown in the following listing.
Listing 4.2 The application’s object graph as part of a console application
public static class Program
{
public static void Main(string[] args) ①
{
string connectionString = args[0]; ②
HomeController controller =
CreateController(connectionString); ③
var result = controller.Index();
var vm = (FeaturedProductsViewModel)result.Model;
Console.WriteLine("Featured products:");
foreach (var product in vm.Products)
{
Console.WriteLine(product.SummaryText);
}
}
private static HomeController CreateController( ④
string connectionString)
{
var userContext = new ConsoleUserContext(); ⑤
return
new HomeController( ⑥ new ProductService( ⑥ new SqlProductRepository( ⑥ new CommerceContext( ⑥ connectionString)), ⑥ userContext)); ⑥
}
}
In this example, the Composition Root is separated from the Main method. This isn’t required, however — the Composition Root isn’t a method or a class, it’s a concept. It can be part of the Main method, or it can span multiple classes, as long as they all reside in a single module. Separating it into its own method helps to ensure that the composition is consolidated and not otherwise interspersed with subsequent application logic.
When you write loosely coupled code, you create many classes to create an application. It can be tempting to compose these classes at many different locations in order to create small subsystems, but that limits your ability to Intercept those systems to modify their behavior. Instead, you should compose classes in one single area of your application.
When you look at Constructor Injection in isolation, you may wonder, doesn’t it defer the decision about selecting a Dependency to another place? Yes, it does, and that’s a good thing. This means that you get a central place where you can connect collaborating classes.
The Composition Root acts as a third party that connects consumers with their services. The longer you defer the decision on how to connect classes, the more you keep your options open. Thus, the Composition Root should be placed as close to the application’s entry point as possible.
即使是使用松散耦合和后期绑定来组合自身的模块化应用程序也有一个包含应用程序入口点的根。示例如下:
Even a modular application that uses loose coupling and late binding to compose itself has a root that contains the entry point into the application. Examples follow:
.NET Core 控制台应用程序是一个包含Program类的库 (.dll)用一种Main方法。
A .NET Core console application is a library (.dll) containing a Program class with a Main method.
一个 ASP.NET Core Web 应用程序也是一个包含带有方法的Program类的库Main.
An ASP.NET Core web application also is a library containing a Program class with a Main method.
UWP和 WPF 应用程序是可执行文件(.exe) 与 App.xaml.cs 文件。
UWP and WPF applications are executables (.exe) with an App.xaml.cs file.
Many other technologies exist, but they have one thing in common: one module contains the entry point of the application — this is the root of the application. Don’t be misled into thinking that the Composition Root is part of your UI layer. Even if you place the Composition Root in the same assembly as your UI layer, as we’ll do in the next example, the Composition Root isn’t part of that layer.
Assemblies are a deployment artifact: you split code into multiple assemblies to allow code to be deployed separately. An architectural layer, on the other hand, is a logical artifact: you can group multiple logical artifacts in a single deployment artifact. Even though the assembly that holds both the Composition Root and the UI layer depends on all other modules in the system, the UI layer itself doesn’t.
It’s not a requirement for the Composition Root to be placed in the same project as your UI layer. You can move the UI layer out of the application’s root project. The advantage of this is that you can prevent the project that holds the UI layer from taking on a dependency (for instance, the data access layer project in chapter 3). This makes it impossible for UI classes to accidentally depend on data access classes. The downside of this approach, however, is that it isn’t always easy to do. With ASP.NET Core MVC, for instance, it’s trivial to move controllers and view models to a separate project, but it can be quite challenging to do the same with your views and client resources.
Separating the presentation technology from the Composition Root might not be that beneficial, either, because a Composition Root is specific to the application. Composition Roots aren’t reused.
You shouldn’t attempt to compose classes in any of the other modules, because that approach limits your options. All classes in application modules should use Constructor Injection (or, in rare cases, one of the other two patterns from this chapter), and then leave it up to the Composition Root to compose the application’s object graph. Any DI Container in use should be limited to the Composition Root.
In an application, the Composition Root should be the sole place that knows about the structure of the constructed object graphs. Application code not only relinquishes control over its Dependencies, it also relinquishes knowledge about its Dependencies. Centralizing this knowledge simplifies development. This also means that application code can’t pass on Dependencies to other threads that run parallel to the current operation, because a consumer has no way of knowing whether it’s safe to do so. Instead, when spinning off concurrent operations, it’s the job of the Composition Root to create a new object graph for each concurrent operation.
The Composition Root in listing 4.2 showed an example of Pure DI. The Composition Root pattern, however, is both applicable to Pure DI and DI Containers. In the next section, we’ll describe how a DI Container can be used in a Composition Root.
As described in chapter 3, a DI Container is a software library that can automate many of the tasks involved in composing objects and managing their lifetimes. But it can be misused as a Service Locator and should only be used as an engine that composes object graphs. When you consider a DI Container from that perspective, it makes sense to constrain it to the Composition Root. This also significantly benefits the removal of any coupling between the DI Container and the rest of the application’s code base.
A Composition Root can be implemented with a DI Container. This means that you use the container to compose the entire application’s object graph in a single call to its Resolve method. When we talk to developers about doing it like this, we can always tell that it makes them uncomfortable because they’re afraid that it’s terribly inefficient and bad for performance. You don’t have to worry about that. That’s almost never the case and, in the few situations where it is, there are ways to address the issue, as we’ll discuss in section 8.4.2.
Don’t worry about the performance overhead of using a DI Container to compose large object graphs. It’s usually not an issue. In part 4, we’ll do a deep dive into DI Containers and show how to use a DI Container inside the Composition Root.
对于基于请求的应用程序,例如网站和服务,您只需配置一次容器,然后为每个传入请求解析一个对象图。第 3 章中的电子商务 Web 应用程序就是一个例子。
When it comes to request-based applications, such as websites and services, you configure the container once, but resolve an object graph for each incoming request. The e-commerce web application in chapter 3 is an example of that.
4.1.3 示例:使用纯 DI实现合成根
4.1.3 Example: Implementing a Composition Root using Pure DI
示例电子商务 Web 应用程序必须有一个组合根来为传入的 HTTP 请求组合对象图。与所有其他 ASP.NET Core Web 应用程序一样,入口点在Main方法中. 然而,默认情况下,MainASP.NET Core 应用程序的方法将大部分工作委托给Startup类. 这个Startup类对我们来说足够接近应用程序的入口点,我们将使用它作为我们的Composition Root。
The sample e-commerce web application must have a Composition Root to compose object graphs for incoming HTTP requests. As with all other ASP.NET Core web applications, the entry point is in the Main method. By default, however, the Main method of an ASP.NET Core application delegates most of the work to the Startup class. This Startup class is close enough to the application’s entry point for us, and we’ll use that as our Composition Root.
As in the previous example with the console application, we use Pure DI. This means you compose your object graphs using plain old C# code instead of a DI Container, as shown in the following listing.
Listing 4.3 The e-commerce application’s Startup class
public class Startup
{
public Startup(IConfiguration configuration)
{
this.Configuration = configuration; ①
}
public IConfiguration Configuration { get; }
public void ConfigureServices( ②
IServiceCollection services)
{
services.AddMvc();
services.AddHttpContextAccessor(); ③ var connectionString = ④ this.Configuration.GetConnectionString( ④ "CommerceConnection"); ④ services.AddSingleton<IControllerActivator>( ⑤ new CommerceControllerActivator( ⑤ connectionString)); ⑤
}
...
}
如果你不熟悉 ASP.NET Core,这里有一个简单的解释:Startup类是必需的;这是您应用所需管道的地方。有趣的部分是. 应用程序的整个设置都封装在类中CommerceControllerActivatorCommerceControllerActivator,我们很快就会展示。
If you’re not familiar with ASP.NET Core, here’s a simple explanation: the Startup class is a necessity; it’s where you apply the required plumbing. The interesting part is the CommerceControllerActivator. The entire setup for the application is encapsulated in the CommerceControllerActivator class, which we’ll show shortly.
要启用将 MVC 控制器连接到应用程序,您必须在 ASP.NET Core MVC 中使用适当的Seam ,称为 an (在 7.3 节中详细讨论)。现在,了解要与 ASP.NET Core MVC 集成就足够了,您必须为组合根创建一个适配器并将其告知框架。IControllerActivator
To enable wiring MVC controllers to the application, you must employ the appropriate Seam in ASP.NET Core MVC, called an IControllerActivator (discussed in detail in section 7.3). For now, it’s enough to understand that to integrate with ASP.NET Core MVC, you must create an Adapter for your Composition Root and tell the framework about it.
Startup.ConfigureServices方法_只运行一次。因此,您的类是仅初始化一次的单个实例。因为您使用自定义设置 ASP.NET Core MVC,所以MVC 调用它的方法CommerceControllerActivatorIControllerActivatorCreate为每个传入的 HTTP 请求创建一个新的控制器实例(您可以在第 7.3 节中阅读详细信息)。以下清单显示了CommerceControllerActivator.
The Startup.ConfigureServices method only runs once. As a result, your CommerceControllerActivator class is a single instance that’s only initialized once. Because you set up ASP.NET Core MVC with the custom IControllerActivator, MVC invokes its Create method to create a new controller instance for each incoming HTTP request (you can read about the details in section 7.3). The following listing shows the CommerceControllerActivator.
Listing 4.4 The application’s IControllerActivator implementation
public class CommerceControllerActivator : IControllerActivator
{
private readonly string connectionString;
public CommerceControllerActivator(string connectionString)
{
this.connectionString = connectionString;
}
public object Create(ControllerContext ctx) ①
{
Type type = ctx.ActionDescriptor
.ControllerTypeInfo.AsType();
if (type == typeof(HomeController)) ②
{
return
new HomeController(
new ProductService(
new SqlProductRepository(
new CommerceContext(
this.connectionString)),
new AspNetUserContextAdapter()));
}
else
{
throw new Exception("Unknown controller."); ③
}
}
}
Notice how the creation of HomeController in this example is almost identical to the application’s object graph from chapter 3 that we showed in listing 4.1. When MVC calls Create, you determine the controller type and create the correct object graph based on this type.
In section 2.3.3, we discussed how only the Composition Root should rely on configuration files, because it’s more flexible for reusable libraries to be imperatively configurable by their callers. You should also separate the loading of configuration values from the methods that do Object Composition (as shown in listings 4.3 and 4.4). The Startup class of listing 4.3 loads the configuration, whereas the CommerceControllerActivator of listing 4.4 only depends on the configuration value, not the configuration system. An important advantage of this separation is that it decouples Object Composition from the configuration system in use, making it possible to test without the existence of a (valid) configuration file.
The Composition Root in this example is spread out across two classes, as shown in figure 4.2. This is expected. The important thing is that all classes are contained in the same module, which, in this case, is the application root.
The most important thing to notice in this figure is that these two classes are the only classes in the entire sample application that compose object graphs. The remaining application code only uses the Constructor Injection pattern.
4.1.4 明显的依赖性爆炸
4.1.4 The apparent dependency explosion
开发人员经常听到的抱怨是,组合根导致应用程序的入口点依赖于应用程序中的所有其他程序集。在他们旧的、紧密耦合的代码库中,他们的入口点只需要依赖于直接在下面的层。这似乎是落后的,因为 DI 旨在减少所需的依赖项数量。他们认为使用 DI 会导致其应用程序入口点的依赖性激增——至少看起来是这样。
An often-heard complaint from developers is that the Composition Root causes the application’s entry point to take a dependency on all other assemblies in the application. In their old, tightly coupled code bases, their entry point only needed to depend on the layer directly below. This seems backward because DI is meant to lower the required number of dependencies. They see the use of DI as causing an explosion of dependencies in their application’s entry point — or so it seems.
这种抱怨源于开发人员误解了项目依赖项的工作方式。为了更好地了解他们担心的是什么,让我们看一下第 2 章中 Mary 应用程序的依赖图,并将其与第 3 章松散耦合应用程序的依赖图(图 4.3)进行比较。
This complaint comes from the fact that developers misunderstand how project dependencies work. To get a good view of what they’re worried about, let’s take a look at the dependency graph of Mary’s application from chapter 2 and compare that with the dependency graph of the loosely coupled application of chapter 3 (figure 4.3).
Figure 4.3 Comparing the dependency graph of Mary’s application to that of the loosely coupled application
乍一看,与 Mary 的应用程序“只有”三个依赖项相比,松散耦合的应用程序中似乎确实多了两个依赖项。然而,该图具有误导性。
At first glance, it indeed looks as if there are two more dependencies in the loosely coupled application, compared to Mary’s application with “only” three dependencies. The diagram, however, is misleading.
Changes to the data access layer also ripple through the UI layer and, as we discussed in the previous chapter, the UI layer can’t be deployed without the data access layer. Even though the diagram doesn’t show it, there’s a dependency between the UI and the data access layer. Assembly dependencies are in fact transitive.
这种传递关系意味着,因为 Mary 的 UI 依赖于域,而域依赖于数据访问,所以 UI 也依赖于数据访问,这正是您在部署应用程序时会遇到的行为。如果您查看 Mary 的应用程序中项目之间的依赖关系,您会发现一些不同的东西(图 4.4)。
This transitive relationship means that because Mary’s UI depends on the domain, and the domain depends on data access, the UI depends on data access too, which is exactly the behavior you’ll experience when deploying the application. If you take a look at the dependencies between the projects in Mary’s application, you’ll see something different (figure 4.4).
Figure 4.4 The dependencies between the libraries in Mary’s application
如您所见,即使在 Mary 的应用程序中,入口点也取决于所有库。Mary 的入口点和松耦合应用程序的组合根具有相同数量的依赖项。但是请记住,依赖性不是由模块的数量定义的,而是由每个模块依赖另一个模块的次数定义的。结果,Mary 的应用程序中所有模块之间的依赖项总数实际上是六个。这比松散耦合的应用程序多了一个。
As you can see, even in Mary’s application, the entry point depends on all libraries. Both Mary’s entry point and the Composition Root of the loosely coupled application have the same number of dependencies. Remember, though, that dependencies aren’t defined by the number of modules, but the number of times each module depends on another module. As a result, the total number of dependencies between all modules in Mary’s application is, in fact, six. That’s one more than the loosely coupled application.
Now imagine an application with dozens of projects. It’s not hard to imagine how the number of dependencies in a tightly coupled code base explodes compared with a loosely coupled code base. But, by writing loosely coupled code that applies the Composition Root pattern, you can lower the number of dependencies. As you’ve seen in the previous chapter, this lets you replace complete modules with different ones, which is harder in a tightly coupled code base.
Composition Root模式适用于所有使用 DI 开发的应用程序,但只有启动项目才会有Composition Root。组合根是消除消费者创建依赖关系的责任的结果。为此,您可以应用两种模式:构造函数注入和财产注入. 构造函数注入是最常见的,应该几乎完全使用。因为构造函数注入是最常用的模式,所以我们接下来会讨论它。
The Composition Root pattern applies to all applications developed using DI, but only startup projects will have a Composition Root. A Composition Root is the result of removing the responsibility for the creation of Dependencies from consumers. To achieve this, you can apply two patterns: Constructor Injection and Property Injection. Constructor Injection is the most common and should be used almost exclusively. Because Constructor Injection is the most commonly used pattern, we’ll discuss that next.
4.2 构造函数注入
4.2 Constructor Injection
我们如何保证我们当前正在开发的类始终可以使用必要的Volatile Dependency ?
How do we guarantee that a necessary Volatile Dependency is always available to the class we’re currently developing?
通过要求所有调用者将Volatile Dependency作为参数提供给类的构造函数。
By requiring all callers to supply the Volatile Dependency as a parameter to the class’s constructor.
When a class requires an instance of a Dependency, you can supply that Dependency through the class’s constructor, enabling it to store the reference for future use.
The constructor signature is compiled with the type and is available for all to see. It clearly documents that the class requires the Dependencies it requests through its constructor. Figure 4.5 demonstrates this.
This figure shows that the consuming class HomeController needs an instance of the IProductServiceDependency to work, so it requires the Composition Root (the client) to supply an instance via its constructor. This guarantees that the instance is available to HomeController when it’s needed.
The class that needs the Dependency must expose a public constructor that takes an instance of the required Dependency as a constructor argument. This should be the only publicly available constructor. If more than one Dependency is needed, additional constructor arguments can be added to the same constructor. Listing 4.5 shows the definition of the HomeController class of figure 4.5.
Listing 4.5 Injecting a Dependency using Constructor Injection
public class HomeController
{
private readonly IProductService service; ① public HomeController( ② IProductService service) ③
{
if (service == null) ④ throw new ArgumentNullException("service"); ④ this.service = service; ⑤
}
}
The IProductServiceDependency is a required constructor argument of HomeController; any client that doesn’t supply an instance of IProductService can’t compile. But, because an interface is a reference type, a caller can pass in null as an argument to make the calling code compile. You need to protect the class against such misuse with a Guard Clause.1 Because the combined efforts of the compiler and the Guard Clause guarantee that the constructor argument is valid if no exception is thrown, the constructor can store the Dependency for future use without knowing anything about the real implementation.
将保存依赖项的字段标记为readonly. 这保证一旦构造函数的初始化逻辑执行完毕,该字段就不能被修改。null从 DI 的角度来看,这并不是严格要求的,但它可以防止您在依赖类代码的其他地方意外修改该字段(例如将其设置为)。
It’s good practice to mark the field holding the Dependency as readonly. This guarantees that once the initialization logic of the constructor has executed, the field can’t be modified. This isn’t strictly required from a DI point of view, but it protects you from accidentally modifying the field (such as setting it to null) somewhere else in the depending class’s code.
When the constructor has returned, the new instance of the class is in a consistent state with a proper instance of its Dependency injected into it. Because the constructed class holds a reference to this Dependency, it can use the Dependency as often as necessary from any of its other members. Its members don’t need to test for null, because the instance is guaranteed to be present.
4.2.2 何时使用构造函数注入
4.2.2 When to use Constructor Injection
构造函数注入应该是 DI 的默认选择。它解决了一个类需要一个或多个Dependencies并且没有合理的Local Defaults可用的最常见场景。
Constructor Injection should be your default choice for DI. It addresses the most common scenario where a class requires one or more Dependencies, and no reasonable Local Defaults are available.
Constructor Injection addresses the common scenario of an object requiring a Dependency with no reasonable Local Default available, because it guarantees that the Dependency must be provided. If the depending class absolutely can’t function without the Dependency, such a guarantee is valuable. Table 4.1 provides a summary of the advantages and disadvantages of Constructor Injection.
In cases where the local library can supply a good default implementation, Property Injection can also be a good fit, but this is usually not the case. In the earlier chapters, we showed many examples of Repositories as Dependencies. These are good examples of Dependencies, where the local library can supply no good default implementation because the proper implementations belong in specialized data access libraries. Apart from the guaranteed injection already discussed, this pattern is also easy to implement using the structure presented in listing 4.5.
The main disadvantage to Constructor Injection is that if the class you’re building is called by your current application framework, you might need to customize that framework to support it. Some frameworks, especially older ones, assume that your classes will have a parameterless constructor.3 (This is called the Constrained Construction anti-pattern, and we’ll discuss this in more detail in the next chapter.) In this case, the framework will need special help creating instances when a parameterless constructor isn’t available. In chapter 7, we’ll explain how to enable Constructor Injection for common application frameworks.
As previously discussed in section 4.1, an apparent disadvantage of Constructor Injection is that it requires that the entire Dependency graph be initialized immediately. Although this sounds inefficient, it’s rarely an issue. After all, even for a complex object graph, we’re typically talking about creating a few dozen new object instances, and creating an object instance is something the .NET Framework does extremely fast. Any performance bottleneck your application may have will appear in other places, so don’t worry about it.4
现在您知道构造函数注入是应用 DI 的首选方式,让我们看一些已知的示例。为此,我们接下来将讨论.NET BCL 中的构造函数注入。
Now that you know that Constructor Injection is the preferred way of applying DI, let’s take a look at some known examples. For this, we’ll discuss Constructor Injection in the .NET BCL next.
4.2.3构造函数注入的已知用法
4.2.3 Known use of Constructor Injection
尽管构造函数注入在使用 DI 的应用程序中普遍存在,但它在 BCL 中并不常见。这主要是因为 BCL 是一组可重用的库,而不是一个成熟的应用程序。您可以在 BCL 中看到某种构造函数注入的两个相关示例是和类System.IO.StreamReaderSystem.IO.StreamWriter. 两者都System.IO.Stream在其构造函数中获取一个实例。这是所有与StreamWritersStream相关的构造函数;StreamReader构造函数是相似的:
Although Constructor Injection tends to be ubiquitous in applications employing DI, it isn’t very present in the BCL. This is mainly because the BCL is a set of reusable libraries and not a full-fledged application. Two related examples where you can see a sort of Constructor Injection in the BCL is with the System.IO.StreamReader and System.IO.StreamWriter classes. Both take a System.IO.Stream instance in their constructors. Here’s all of StreamWriter's Stream-related constructors; the StreamReader constructors are similar:
public StreamWriter(Stream stream);
public StreamWriter(Stream stream, Encoding encoding);
public StreamWriter(Stream stream, Encoding encoding, int bufferSize);
Stream is an abstract class that serves as an Abstraction on which StreamWriter and StreamReader operate to perform their duties. You can supply any Stream implementation in their constructors, and they’ll use it, but they’ll throw ArgumentNullExceptions if you try to slip them a null stream.
Although the BCL provides examples where you can see Constructor Injection in use, it’s always more instructive to see a working example. The next section walks you through a full implementation example.
4.2.4 示例:向特色产品添加货币换算
4.2.4 Example: Adding currency conversions to the featured products
Mary’s boss says her app is working fine, but now some customers who are using it want to pay for goods in different currencies. Can she write some new code that enables the app to display and calculate costs in different currencies? Mary sighs and realizes that it’s not going to be enough to hard-code in a few different currency conversions. She’ll need to write code flexible enough to accommodate any currency over time. DI is calling again.
Mary 需要的既是表示货币及其货币的对象,又是允许将货币从一种货币转换为另一种货币的抽象。她会给抽象 命名。为简单起见, the将只有一个 currency ,并且由 a和 an组成,如图 4.6所示。ICurrencyConverterCurrencyCodeMoneyCurrencyAmount
What Mary needs is both an object for representing money and its currency and an Abstraction that allows converting money from one currency into another. She’ll name the AbstractionICurrencyConverter. For simplicity, the Currency will only have a currency Code, and Money is composed of both a Currency and an Amount, as shown in figure 4.6.
Listing 4.6Currency, Money, and the ICurrencyConverter interface
public interface ICurrencyConverter
{
Money Exchange(Money money, Currency targetCurrency);
}
public class Currency
{
public readonly string Code;
public Currency(string code)
{
if (code == null) throw new ArgumentNullException("code");
this.Code = code;
}
}
public class Money
{
public readonly decimal Amount;
public readonly Currency Currency;
public Money(decimal amount, Currency currency)
{
if (currency == null) throw new ArgumentNullException("currency");
this.Amount = amount;
this.Currency = currency;
}
}
可能表示进程外资源,例如 Web 服务或提供转换率的数据库。这意味着在单独的项目(例如数据访问层)中实现具体的内容是合适的。因此,没有合理的本地默认值ICurrencyConverterICurrencyConverter.
An ICurrencyConverter is likely to represent an out-of-process resource, such as a web service or a database that supplies conversion rates. This means that it’d be fitting to implement a concrete ICurrencyConverter in a separate project, such as a data access layer. Hence, there’s no reasonable Local Default.
At the same time, the ProductService class will need an ICurrencyConverter. Constructor Injection is a good fit. The following listing shows how the ICurrencyConverterDependency is injected into ProductService.
Listing 4.7 Injecting an ICurrencyConverter into ProductService
public class ProductService : IProductService
{
private readonly IProductRepository repository;
private readonly IUserContext userContext;
private readonly ICurrencyConverter converter;
public ProductService(
IProductRepository repository,
IUserContext userContext,
ICurrencyConverter converter)
{
if (repository == null)
throw new ArgumentNullException("repository");
if (userContext == null)
throw new ArgumentNullException("userContext");
if (converter == null)throw new ArgumentNullException("converter");
this.repository = repository;
this.userContext = userContext;
this.converter = converter;
}
}
因为ProductService班已经有了对 and 的依赖,我们添加新的依赖作为第三个构造函数参数,然后按照清单 4.5中列出的相同顺序进行操作。警卫IProductRepositoryIUserContextICurrencyConverterClauses 保证Dependencies不为 null,这意味着将它们存储起来以供以后在只读字段中使用是安全的。因为 anICurrencyConverter保证存在于 中ProductService,所以它可以在任何地方使用;例如,在GetFeaturedProducts方法中如此处所示。
Because the ProductService class already had a Dependency on IProductRepository and IUserContext, we add the new ICurrencyConverterDependency as a third constructor argument and then follow the same sequence outlined in listing 4.5. Guard Clauses guarantee that the Dependencies aren’t null, which means it’s safe to store them for later use in read-only fields. Because an ICurrencyConverter is guaranteed to be present in ProductService, it can be used from anywhere; for example, in the GetFeaturedProducts method as shown here.
Listing 4.8ProductService using ICurrencyConverter
public IEnumerable<DiscountedProduct> GetFeaturedProducts()
{
Currency userCurrency = this.userContext.Currency; ①
var products =
this.repository.GetFeaturedProducts();
return
from product in products
let unitPrice = product.UnitPrice ② let amount = this.converter.Exchange( ③ money: unitPrice, ③ targetCurrency: userCurrency) ③
select product
.WithUnitPrice(amount)
.ApplyDiscountFor(this.userContext);
}
请注意,您可以使用该converter字段无需提前检查其可用性。那是因为它保证存在。
Notice that you can use the converter field without needing to check its availability in advance. That’s because it’s guaranteed to be present.
4.2.5 总结
4.2.5 Wrap-up
构造函数注入是可用的最普遍适用的 DI 模式,也是最容易正确实现的模式。它适用于需要依赖项时。如果您需要使依赖项成为可选的,您可以更改为Property Injection如果它有一个合适的Local Default。
Constructor Injection is the most generally applicable DI pattern available, and also the easiest to implement correctly. It applies when the Dependency is required. If you need to make the Dependency optional, you can change to Property Injection if it has a proper Local Default.
The next pattern in this chapter is Method Injection, which takes a slightly different approach. It tends to apply more to the situation where you already have a Dependency that you want to pass on to the collaborators you invoke.
4.3 方法注入
4.3 Method Injection
当每个操作都不同时,我们如何将依赖项注入到类中?
How can we inject a Dependency into a class when it’s different for each operation?
In cases where a Dependency can vary with each method call, or the consumer of such a Dependency can vary on each call, you can supply a Dependency via a method parameter.
Figure 4.7 Using Method Injection, ProductService creates an instance of Product and injects an instance of IUserContext into Product.ApplyDiscountFor with each method call.
The caller supplies the Dependency as a method parameter in each method call. An example of this approach in Mary’s e-commerce application is in the Product class, where the ApplyDiscountFor method accepts an IUserContextDependency using Method Injection:
IUserContext presents contextual information for the operation to run, which is a common scenario for Method Injection. Often this context will be supplied to a method alongside a “proper” value, as shown in listing 4.9.
The price value parameter represents the value on which the method is supposed to operate, whereas context contains information about the current context of the operation; in this case, information about the current user. The caller supplies the Dependency to the method. As you’ve seen many times before, the Guard Clause guarantees that the context is available to the rest of the method body.
4.3.2 何时使用方法注入
4.3.2 When to use Method Injection
方法注入不同于其他类型的 DI 模式,因为注入不是在组合根中发生,而是在调用时动态发生。这允许调用者提供特定于操作的上下文,这是 .NET BCL 中使用的常见扩展机制。表 4.2总结了方法注入的优点和缺点。
Method Injection is different from other types of DI patterns in that the injection doesn’t happen in a Composition Root but, rather, dynamically at invocation. This allows the caller to provide an operation-specific context, which is a common extensibility mechanism used in the .NET BCL. Table 4.2 provides a summary of the advantages and disadvantages of Method Injection.
The following sections show an example of each. Listing 4.9 is an example of how the consumer varies. This is the most common form, which is why we’ll start with providing another example.
示例:在每次方法调用时改变Dependency的使用者
Example: Varying the Dependency's consumer on each method call
When you practice Domain-Driven Design (DDD), it’s common to create domain Entities that contain domain logic, effectively mixing runtime data with behavior in the same class.8Entities, however, are typically not created within the Composition Root. Take the following CustomerEntity, for example.
Listing 4.10 An Entity containing domain logic but no Dependencies (yet)
public class Customer ①
{
public Guid Id { get; private set; } ② public string Name { get; private set; } ② public Customer(Guid id, string name) ③
{
...
}
public void RedeemVoucher(Voucher voucher) ... ④ public void MakePreferred() ... ⑤
}
The RedeemVoucher and MakePreferred methods in listing 4.10 are domain methods. RedeemVoucher implements the domain logic that lets the customer redeem a voucher. (You may have redeemed a voucher to get a discount when you purchased this book.) voucher is a value object9 used by the method. MakePreferred, on the other hand, implements the domain logic that promotes the customer. A regular customer could get upgraded to become a preferred customer, which might give certain advantages and discounts, similar to being a frequent flyer airline customer.
Entities that contain behavior besides their usual set of data members would easily get a wide range of methods, each requiring their own Dependencies. Although you might be tempted to use Constructor Injection to inject such Dependencies, that leads to a situation where each such Entity needs to be created with all of its Dependencies, even though only a few may be necessary for a given use case. This complicates testing the logic of an Entity, because all Dependencies need to be supplied to the constructor, even though a test might only be interested in a few Dependencies. Method Injection, as shown in the next listing, offers a better alternative.
public class Customer
{
public Guid Id { get; private set; }
public string Name { get; private set; }
public Customer(Guid id, string name)
{
...
}
public void RedeemVoucher( ①
Voucher voucher,
IVoucherRedemptionService service)
{
if (voucher == null)
throw new ArgumentNullException("voucher");
if (service == null)
throw new ArgumentNullException("service");
service.ApplyRedemptionForCustomer(
voucher,
this.Id);
}
public void MakePreferred(IEventHandler handler) ①
{
if (handler == null)
throw new ArgumentNullException("handler");
handler.Publish(new CustomerMadePreferred(this.Id));
}
}
Inside a CustomerServices component, the Customer's RedeemVoucher method can be called while passing the IVoucherRedemptionServiceDependency with the call, as shown next.
In listing 4.12, only a single Customer instance is requested from ICustomerRepository. But a single CustomerServices instance can be called over and over again using a multitude of customers and vouchers, causing the same IVoucherRedemptionService to be supplied to many different Customer instances. Customer is the consumer of the IVoucherRedemptionServiceDependency and, while you’re reusing the Dependency, you’re varying the consumer.
This is similar to the first Method Injection example shown in listing 4.9 and the ApplyDiscountFor method discussed in listing 3.8. The opposite case is when you vary the Dependency while keeping its consumers around.
示例:在每个方法调用上改变注入的依赖项
Example: Varying the injected Dependency on each method call
Imagine an add-in system for a graphical drawing application, where you want everyone to be able to plug in their own image effects. External image effects might require information about the runtime context, which can be passed on by the application to the image effect. This is a typical use case for applying Method Injection. You can define the following interface for applying those effects:
public interface IImageEffectAddIn ①
{
Bitmap Apply( ②
Bitmap source,
IApplicationContext context); ③
}
The IImageEffectAddIn's IApplicationContextDependency can vary with each call to the Apply method, providing the effect with information about the context in which the operation is being invoked. Any class implementing this interface can be used as an add-in. Some implementations may not care about the context at all, whereas other implementations will.
public Bitmap ApplyEffects(Bitmap source)
{
if (source == null) throw new ArgumentNullException("source");
Bitmap result = source;
foreach (IImageEffectAddIn effect in this.effects)
{
result = effect.Apply(result, this.context);
}
return result;
}
The private effects field is a list of IImageEffectAddIn instances, which allows the client to loop through the list to invoke each add-in’s Apply method. Each time the Apply method is invoked on an add-in, the operation’s context, represented by the context field, is passed as a method parameter:
At times, the value and the operational context are encapsulated in a single Abstraction that works as a combination of both. An important thing to note is this: as you’ve seen in both examples, the Dependency injected via Method Injection becomes part of the definition of the Abstraction. This is typically desirable in case that Dependency contains runtime information that’s supplied by its direct callers.
In cases where the Dependency is an implementation detail to the caller, you should try to prevent the Abstraction from being “polluted”; therefore, Constructor Injection is a better pick. Otherwise, you could easily end up passing the Dependency from the top of our application’s object graph all the way down, causing sweeping changes.
The previous examples all showed the use of Method Injection outside of the Composition Root. This is deliberate. Method Injection is unsuitable when used within the Composition Root. Within a Composition Root, Method Injection can initialize a previously constructed class with its Dependencies. Doing so, however, leads to Temporal Coupling and for that reason it’s highly discouraged.
时间耦合代码的味道
The Temporal Coupling code smell
时间耦合是 API 设计中的一个常见问题。当一个类的两个或多个成员之间存在隐式关系时,就会发生这种情况,要求客户先调用一个成员再调用另一个。这在时间维度上紧密耦合了成员。原型示例是使用Initialize方法,尽管可以找到大量其他示例——甚至在 BCL 中也是如此。例如,这种用法编译但在运行时失败:System.ServiceModel.EndpointAddressBuilder
Temporal Coupling is a common problem in API design. It occurs when there’s an implicit relationship between two or more members of a class, requiring clients to invoke one member before the other. This tightly couples the members in the temporal dimension. The archetypical example is the use of an Initialize method, although copious other examples can be found — even in the BCL. As an example, this usage of System.ServiceModel.EndpointAddressBuilder compiles but fails at runtime:
var builder = new EndpointAddressBuilder();
var address = builder.ToEndpointAddress();
事实证明,在创建 之前需要一个 URI。以下代码在运行时编译并成功:EndpointAddress
It turns out that an URI is required before an EndpointAddress can be created. The following code compiles and succeeds at runtime:
var builder = new EndpointAddressBuilder();
builder.Uri = new UriBuilder().Uri;
var address = builder.ToEndpointAddress();
API 没有暗示这是必要的,但属性之间存在时间耦合Uri和ToEndpointAddress方法.
The API provides no hint that this is necessary, but there’s a Temporal Coupling between the Uri property and the ToEndpointAddress method.
public class Component
{
private ISomeInterface dependency;
public void Initialize( ① ISomeInterface dependency) ①
{
this.dependency = dependency;
}
public void DoSomething()
{
if (this.dependency == null) ② throw new InvalidOperationException( ② "Call Initialize first."); ②
this.dependency.DoStuff();
}
}
在语义上,方法的名称是一个线索,但在结构层面上,这个 API 没有给我们任何Temporal CouplingInitialize的迹象。因此,像这样的代码可以编译,但会在运行时抛出异常:
Semantically, the name of the Initialize method is a clue, but on a structural level, this API gives us no indication of Temporal Coupling. Thus, code like this compiles, but throws an exception at runtime:
var c = new Component();
c.DoSomething();
这个问题的解决方案现在应该很明显了——你应该应用构造函数注入来代替:
The solution to this problem should be obvious by now — you should apply Constructor Injection instead:
public class Component
{
private readonly ISomeInterface dependency;
public Component(ISomeInterface dependency)
{
if (dependency == null)
throw new ArgumentNullException("dependency");
this.dependency = dependency;
}
public void DoSomething()
{
this.dependency.DoStuff();
}
}
The .NET BCL provides many examples of Method Injection, particularly in the System.ComponentModel namespace. You use System.ComponentModel.Design.IDesigner for implementing custom design-time functionality for components. It has an Initialize method that takes an IComponent instance so that it knows which component it’s currently helping to design. (Note that this Initialize method causes Temporal Coupling.) Designers are created by IDesignerHost implementations that also take IComponent instances as parameters to create designers:
This is a good example of a scenario where the parameter itself carries information. The component can carry information about which IDesigner to create, but at the same time, it’s also the component on which the designer must subsequently operate.
Another example in the System.ComponentModel namespace is provided by the TypeConverter class. Several of its methods take an instance of ITypeDescriptorContext that, as the name says, conveys information about the context of the current operation, such as information about the type’s properties. Because there are many such methods, we don’t want to list them all, but here’s a representative example:
public virtual object ConvertTo(ITypeDescriptorContext context,
CultureInfo culture, object value, Type destinationType)
In this method, the context of the operation is communicated explicitly by the context parameter, whereas the value to be converted and the destination type are sent as separate parameters. Implementers can use or ignore the context parameter as they see fit.
ASP.NET Core MVC 还包含几个方法注入示例。你可以使用IValidationAttributeAdapterProvider界面,例如,提供实例。它唯一的方法是这样的:IAttributeAdapter
ASP.NET Core MVC also contains several examples of Method Injection. You can use the IValidationAttributeAdapterProvider interface, for instance, to provide IAttributeAdapter instances. Its only method is this:
ASP.NET Core 允许将视图模型的属性标记为. 这是一种应用元数据的便捷方式,元数据描述封装在视图模型中的属性的有效性。ValidationAttribute
ASP.NET Core allows properties of view models to be marked with ValidationAttribute. It’s a convenient way to apply metadata that describes the validity of properties encapsulated in the view model.
Based on a ValidationAttribute, the GetAttributeAdapter method allows an IAttributeAdapter to be returned, which allows relevant error messages to be displayed in a web page. In the GetAttributeAdapter method, the attribute parameter is the object an IAttributeAdapter should be created for, whereas the stringLocalizer is the Dependency that’s passed through Method Injection.
接下来,我们将看到 Mary 如何使用方法注入来防止代码重复。当我们最后一次见到 Mary(在 4.2 节)时,她正在做:她使用构造函数注入将它注入到类中。ICurrencyConverterProductService
Next, we’ll see how Mary uses Method Injection in order to prevent code repetition. When we last saw Mary (in section 4.2), she was working on ICurrencyConverter: she injected it using Constructor Injection into the ProductService class.
4.3.4 示例:向Product实体添加货币换算
4.3.4 Example: Adding currency conversions to the ProductEntity
清单 4.8展示了该GetFeaturedProducts方法是如何调用了ICurrencyConverter.Exchange方法在 Mary 的应用程序中使用产品UnitPrice和用户的首选货币。又是这个GetFeaturedProducts方法:
Listing 4.8 showed how the GetFeaturedProducts method called the ICurrencyConverter.Exchange method using the product’s UnitPrice and the user’s preferred currency in Mary’s application. Here’s that GetFeaturedProducts method again:
public IEnumerable<DiscountedProduct> GetFeaturedProducts()
{
Currency currency = this.userContext.Currency;
return
from product in this.repository.GetFeaturedProducts()
let amount = this.converter.Exchange(product.UnitPrice, currency)
select product
.WithUnitPrice(amount)
.ApplyDiscountFor(this.userContext);
}
Conversions of ProductEntities from one Currency to another will be a recurring task in many parts of her application. For this reason, Mary likes to move the logic concerning the conversion of the Product out of ProductService and centralize it as part of the ProductEntity. This prevents other parts of the system from repeating this code. Method Injection turns out to be a great candidate for this. Mary creates a new ConvertTo method in Product, as shown in the next listing.
public class Product
{
public string Name { get; set; }
public Money UnitPrice { get; set; }
public bool IsFeatured { get; set; }
public Product ConvertTo(
Currency currency, ① ICurrencyConverter converter) ②
{
if (currency == null)
throw new ArgumentNullException("currency");
if (converter == null)
throw new ArgumentNullException("converter");
var newUnitPrice =
converter.Exchange( ③
this.UnitPrice,
currency);
return this.WithUnitPrice(newUnitPrice); ④
}
public Product WithUnitPrice(Money unitPrice)
{
return new Product
{
Name = this.Name,
UnitPrice = unitPrice,
IsFeatured = this.IsFeatured
};
}
...
}
使用新ConvertTo方法,Mary 重构了GetFeaturedProducts方法.
With the new ConvertTo method, Mary refactors the GetFeaturedProducts method.
Listing 4.16GetFeaturedProducts using ConvertTo method
public IEnumerable<DiscountedProduct> GetFeaturedProducts()
{
Currency currency = this.userContext.Currency;
return
from product in this.repository.GetFeaturedProducts()
select product
.ConvertTo(currency, this.converter) ①
.ApplyDiscountFor(this.userContext);
}
而不是调用ICurrencyConverter.Exchange方法,如您之前所见,现在传递给使用方法注入的方法。当 Mary 需要在她的代码库中的其他地方转换产品时,这简化了方法并防止了任何代码重复。通过使用方法注入而不是构造函数注入,她避免了必须构建具有所有依赖项的实体。这简化了构造和测试。GetFeaturedProductsICurrencyConverterConvertToGetFeaturedProductsProduct
Instead of calling the ICurrencyConverter.Exchange method, as you’ve seen previously, GetFeaturedProducts now passes ICurrencyConverter on to the ConvertTo method using Method Injection. This simplifies the GetFeaturedProducts method and prevents any code duplication when Mary needs to convert products elsewhere in her code base. By using Method Injection instead of Constructor Injection, she avoided having to build up the ProductEntity with all of its Dependencies. This simplifies construction and testing.
与本章中的其他 DI 模式不同,当您想要向已经存在的消费者提供依赖项时,您主要使用方法注入。另一方面,使用Constructor Injection和Property Injection ,您可以在消费者创建时向其提供依赖项。
Unlike the other DI patterns in this chapter, you mainly use Method Injection when you want to supply Dependencies to an already existing consumer. With Constructor Injection and Property Injection, on the other hand, you supply Dependencies to a consumer while it’s being created.
The last pattern in this chapter is Property Injection, which allows you to override a class’s Local Default. Where Method Injection was solely applied outside the Composition Root, Property Injection, just as Constructor Injection, is applied from within the Composition Root.
4.4 属性注入
4.4 Property Injection
当我们有一个好的Local Default时,我们如何在类中启用 DI 作为一个选项?
How do we enable DI as an option in a class when we have a good Local Default?
通过公开一个可写属性,让调用者在想要覆盖默认行为时提供依赖项。
By exposing a writable property that lets callers supply a Dependency if they want to override the default behavior.
When a class has a good Local Default, but you still want to leave it open for extensibility, you can expose a writable property that allows a client to supply a different implementation of the class’s Dependency than the default. As figure 4.8 shows, clients wanting to use the Consumer class as is can create an instance of the class and use it without giving it a second thought, whereas clients wanting to modify the behavior of the class can do so by setting the Dependency property to a different implementation of IDependency.
The class that uses the Dependency must expose a public writable property of the Dependency’s type. In a bare-bones implementation, this can be as simple as the following listing.
Unfortunately, such an implementation is fragile. That’s because the Dependency property isn’t guaranteed to return an instance of IDependency. Code like this would throw a NullReferenceException if the value of the Dependency property is null:
var instance = new Consumer();
instance.DoSomething(); ①
This issue can be solved by letting the constructor set a default instance on the property, combined with a proper Guard Clause in the property’s setter. Another complication arises if clients switch the Dependency in the middle of the class’s lifetime:
var instance = new Consumer();
instance.Dependency = new SomeImplementation(); ①
instance.DoSomething();
instance.Dependency = new SomeOtherImplementation(); ②
instance.DoSomething();
This can be addressed by introducing an internal flag that only allows a client to set the Dependency during initialization.11
4.4.4 节中的示例展示了如何处理这些并发症。但在此之前,我们想解释一下何时适合使用属性注入。
The example in section 4.4.4 shows how you can deal with these complications. But before we get to that, we’d like to explain when it’s appropriate to use Property Injection.
Property Injection should only be used when the class you’re developing has a good Local Default, and you still want to enable callers to provide different implementations of the class’s Dependency. It’s important to note that Property Injection is best used when the Dependency is optional. If the Dependency is required, Constructor Injection is always a better pick.
In chapter 1, we discussed good reasons for writing code with loose coupling, thus isolating modules from each other. But loose coupling can also be applied to classes within a single module with great success. This is often done by introducing Abstractions within a module and letting classes within that module communicate via Abstractions, instead of being tightly coupled to each other. The main reasons for applying loose coupling within a module boundary is to open classes for extensibility and for ease of testing.
We haven’t shown you any real examples of Property Injection so far because the applicability of this pattern is more limited, especially in the context of application development. Table 4.3 summarizes its advantages and disadvantages.
The main advantage of Property Injection is that it’s so easy to understand. We’ve often seen this pattern used as a first attempt when people decide to adopt DI.
Appearances can be deceptive, though, and Property Injection is fraught with difficulties. It’s challenging to implement it in a robust manner. Clients can forget to supply the Dependency because of the previously discussed problem of Temporal Coupling. Additionally, what would happen if a client tries to change the Dependency in the middle of the class’s lifetime? This could lead to inconsistent or unexpected behavior, so you may want to protect yourself against that event.
尽管存在缺点,但在构建可重用库时使用属性注入是有意义的。它允许组件定义合理的默认值,这简化了使用库 API 的工作。
Despite the downsides, it makes sense to use Property Injection when building a reusable library. It allows components to define sensible defaults, and this simplifies working with a library’s API.
When developing applications, you wire up your classes in your Composition Root. Constructor Injection prevents you from forgetting to supply the Dependency. Even in the case that there’s a Local Default, such instances can be supplied to the constructor by the Composition Root. This simplifies the class and allows the Composition Root to be in control over the value that all consumers get. This might even be a Null Object implementation.
The existence of a good Local Default depends in part on the granularity of modules. The BCL ships as a rather large package; as long as the default stays within the BCL, it could be argued that it’s also local. In the next section, we’ll briefly touch on that subject.
In the .NET BCL, Property Injection is a bit more common than Constructor Injection, probably because good Local Defaults are defined in many places, and also because this simplifies the default instantiation of most classes. For example, System.ComponentModel.IComponent has a writable Site property that allows you to define an ISite instance. This is mostly used in design time scenarios (for example, by Visual Studio) to alter or enhance a component when it’s hosted in a designer. With that BCL example as an appetizer, let’s move on to a more substantial example of using and implementing Property Injection.
4.4.4 示例:属性注入作为可重用库的扩展模型
4.4.4 Example: Property Injection as an extensibility model of a reusable library
Earlier examples in this chapter extended the sample application of the previous chapter. Although we could show you an example of Property Injection using the sample application, this would be misleading because Property Injection is hardly ever a good fit when building applications; Constructor Injection is almost always a better choice. Instead, we’d like to show you an example of a reusable library. In this case, we’re looking at some code from Simple Injector.
Simple Injector is one of the DI Containers that’s discussed in part 4. It helps you construct your application’s object graphs. Chapter 14 will have an extensive discussion on Simple Injector, so we won’t go into much detail about it here. From the perspective of Property Injection, how Simple Injector works isn’t important.
As a reusable library, Simple Injector makes extensive use of Property Injection. Lots of its behavior can be extended, and the way this is done is by providing default implementations of its behavior. Simple Injector exposes properties that allow the user to change the default implementation. One of the behaviors that Simple Injector allows to be replaced is how the library selects the correct constructor for doing Constructor Injection.14
As we discussed in section 4.2, your classes should only have one constructor. Because of this, Simple Injector, by default, only allows classes that have just one public constructor to be created. In any other case, Simple Injector throws an exception. Simple Injector, however, lets you override this behavior. This might be useful for certain narrow integration scenarios. For this, Simple Injector defines an IConstructorResolutionBehavior interface.15 A custom implementation can be defined by the user, and the library-provided default can be replaced by setting the ConstructorResolutionBehavior property, as shown here:
var container = new Container();
container.Options.ConstructorResolutionBehavior =
new CustomConstructorResolutionBehavior();
这Container是 Simple Injector API 中的中心 Facade 16 模式。它用于指定抽象和实现之间的关系,并构建这些实现的对象图。该类包含一个Options属性的类型。它包括许多允许更改库的默认行为的属性和方法。这些属性之一是. 这是该课程的简化版本ContainerOptionsConstructorResolutionBehaviorContainerOptions及其ConstructorResolutionBehavior财产:
The Container is the central Facade16 pattern in Simple Injector’s API. It’s used to specify the relationships between Abstractions and implementations, and to build object graphs of these implementations. The class includes an Options property of type ContainerOptions. It includes a number of properties and methods that allow the default behavior of the library to be changed. One of those properties is ConstructorResolutionBehavior. Here’s a simplified version of the ContainerOptions class with its ConstructorResolutionBehavior property:
public class ContainerOptions
{
IConstructorResolutionBehavior resolutionBehavior =
new DefaultConstructorResolutionBehavior(); ①
public IConstructorResolutionBehavior ConstructorResolutionBehavior
{
get
{
return this.resolutionBehavior;
}
set
{
if (value == null) ②
throw new ArgumentNullException("value");
if (this.Container.HasRegistrations) ③
{
throw new InvalidOperationException(
"The ConstructorResolutionBehav" +
"ior property cannot be changed" +
" after the first registration " +
"has been made to the container.";
}
this.resolutionBehavior = value; ④
}
}
}
The ConstructorResolutionBehavior property can be changed multiple times as long as there are no registrations made in the container. This is important, because when registrations are made, Simple Injector uses the specified ConstructorResolutionBehavior to verify whether it’ll be able to construct such a type by analyzing a class’s constructor. If a user was able to change the constructor resolution behavior after registrations were made, it could impact the correctness of earlier registrations. This is because Simple Injector could, otherwise, end up using a different constructor for a component from that which it approved to be correct during the time of registration. This means that either all previous registrations should be reevaluated or the user should be prevented from being able to change the behavior after registrations are made. Because reevaluating can have hidden performance costs and is harder to implement, Simple Injector implements the latter approach.
Compared to Constructor Injection, Property Injection is more involved. It may look simple in its raw form (as shown in listing 4.19), but, properly implemented, it tends to be more complex.
You use Property Injection in a reusable library where the Dependency is optional and you have a good Local Default. In cases where there’s a short-lived object that requires the Dependency, you should use Method Injection. In other cases, you should use Constructor Injection.
这完成了本章的最后一个模式。以下部分提供了一个简短的回顾,并解释了如何为您的工作选择正确的模式。
This completes the last pattern in this chapter. The following section provides a short recap and explains how to select the right pattern for your job.
4.5 选择要使用的模式
4.5 Choosing which pattern to use
本章介绍的模式是 DI 的核心部分。有了Composition Root和适当的注入模式组合,您就可以实现Pure DI或使用DI Container。应用 DI 时,有许多细微差别和更精细的细节需要学习,但这些模式涵盖了回答“如何注入我的依赖项?”这个问题的核心机制。
The patterns presented in this chapter are a central part of DI. Armed with a Composition Root and an appropriate mix of injection patterns, you can implement Pure DI or use a DI Container. When applying DI, there are many nuances and finer details to learn, but these patterns cover the core mechanics that answer the question, “How do I inject my Dependencies?”
These patterns aren’t interchangeable. In most cases, your default choice should be Constructor Injection, but there are situations where one of the other patterns affords a better alternative. Figure 4.9 shows a decision process that can help you decide on a proper pattern, but, if in doubt, choose Constructor Injection. You can’t go terribly wrong with that choice.
图 4.9 模式决策过程。在大多数情况下,您应该选择Constructor Injection,但在某些情况下,其他 DI 模式之一更适合。
Figure 4.9 Pattern decision process. In most cases, you should choose Constructor Injection, but there are situations where one of the other DI patterns is a better fit.
The first thing to examine is whether the Dependency is something you need or something you already have but want to communicate to another collaborator. In most cases, you’ll probably need the Dependency. But in add-in scenarios, you may want to convey the current context to an add-in. Every time the Dependency varies from operation to operation, Method Injection is a good candidate for an implementation.
Secondly, you’ll need to know what kind of class needs the Dependency. In case you’re mixing runtime data with behavior in the same class, as you might do in your domain Entities, Method Injection is a good fit. In other cases, when you’re writing application code, opposed to writing a reusable library, Constructor Injection automatically applies.
When it comes to writing application code, even the use of Local Defaults should be prevented in favor of having these defaults set in one central place in the application — the Composition Root. On the other hand, when writing a reusable library, a Local Default is the deciding factor, as it can make explicitly assigning the Dependency optional — the default takes over if no overriding implementation is specified. This scenario can be effectively implemented with Property Injection.
构造函数注入应该是 DI 的默认选择。与任何其他 DI 模式相比,它易于理解且更易于稳健地实现。您可以单独使用构造函数注入构建整个应用程序,但了解其他模式可以帮助您在不完全适合的少数情况下做出明智的选择。下一章从相反的方向探讨 DI,并考察使用 DI 的不明智方式。
Constructor Injection should be your default choice for DI. It’s easy to understand and simpler to implement robustly than any of the other DI patterns. You can build entire applications with Constructor Injection alone, but knowing about the other patterns can help you choose wisely in the few cases where it doesn’t fit perfectly. The next chapter approaches DI from the opposite direction and takes a look at ill-advised ways of using DI.
The Composition Root is a single, logical location in an application where modules are composed together. The construction of your application’s components should be concentrated into this single area of your application.
只有启动项目才会有Composition Root。
Only startup projects will have a Composition Root.
尽管组合根可以分布在多个类中,但它们应该位于单个模块中。
Although a Composition Root can be spread out across multiple classes, they should be in a single module.
The Composition Root takes a direct dependency on all other modules in the system. Loosely coupled code that applies the Composition Root pattern lowers the overall number of dependencies between modules, subsystems, and layers, compared to tightly coupled code.
Even though you might place the Composition Root in the same assembly as your UI or presentation layer, the Composition Root isn’t part of those layers. Assemblies are deployment artifacts, whereas layers are logical artifacts.
Where a DI Container is used, it should only be referenced from the Composition Root. All other modules should be oblivious to the existence of the DI Container.
在组合根之外使用DI 容器会导致服务定位器反模式。
Use of a DI Container outside the Composition Root leads to the Service Locator anti-pattern.
使用DI 容器组合大型对象图的性能开销在设计良好的系统中通常不是问题。
The performance overhead of using a DI Container to compose large object graphs is usually not an issue in a well-designed system.
The Composition Root should be the sole place in the entire application that knows about the structure of the constructed object graphs. This means that application code can’t pass on Dependencies to other threads that run parallel to the current operation, because a consumer has no way of knowing whether it’s safe to do so. Instead, when spinning off concurrent operations, it’s the Composition Root’s job to create a new object graph for each concurrent operation.
构造函数注入是通过将所需依赖项指定为类构造函数的参数来静态定义所需依赖项列表的行为。
Constructor Injection is the act of statically defining the list of required Dependencies by specifying them as parameters to the class’s constructor.
A constructor that’s used for Constructor Injection should do no more than apply Guard Clauses and store the receiving Dependencies. Other logic should be kept out of the constructor. This makes building object graphs fast and reliable.
构造函数注入应该是 DI 的默认选择,因为它是最可靠且最容易正确应用的。
Constructor Injection should be your default choice for DI, because it’s the most reliable and the easiest to apply correctly.
Constructor Injection is well suited when a Dependency is required. It’s important to note, however, that Dependencies should hardly ever be optional. Optional Dependencies complicate the component with null checks. Inside the Composition Root, a Null Object implementation should instead be injected when there’s no reasonable implementation available.
Application components should only have a single constructor. Overloaded constructors lead to ambiguity. For reusable class libraries, like the BCL, having multiple constructors often makes sense; for application components, it doesn’t.
方法注入是在方法调用上传递依赖关系的行为。
Method Injection is the act of passing Dependencies on method invocations.
如果Dependency或Dependency的消费者对于每个操作可能不同,您可以应用Method Injection。这对于需要将某些运行时上下文传递到加载项的公共 API 的加载项场景,或者当以数据为中心的对象需要特定操作的依赖项时(域实体通常就是这种情况),这可能很有用.
Where either a Dependency or a Dependency’s consumer can differ for each operation, you can apply Method Injection. This can be useful for add-in scenarios where some runtime context needs to be passed along to the add-in’s public API, or when a data-centric object requires a Dependency for a certain operation, as will often be the case with domain Entities.
方法注入不适合在组合根内部使用,因为它会导致时间耦合。
Method Injection is unsuited for use inside the Composition Root because it leads to Temporal Coupling.
A method that accepts a Dependency through Method Injection shouldn’t store that Dependency. This leads to Temporal Coupling, Captive Dependencies, or hidden side effects. Dependencies should only be stored with Constructor Injection and Property Injection.
本地默认是源自同一模块或层的依赖项的默认实现。
A Local Default is a default implementation of a Dependency that originates in the same module or layer.
属性注入允许类库为扩展而开放,因为它允许调用者改变库的默认行为。
Property Injection allows class libraries to be open for extension, because it lets callers change the library’s default behavior.
Beyond optional Dependencies within reusable libraries, the applicability of Property Injection is limited, and Constructor Injection is usually a better fit. Constructor Injection simplifies the class, allows the Composition Root to be in control over the value that all consumers get, and prevents Temporal Coupling.
5
DI 反模式
5
DI anti-patterns
在这一章当中
In this chapter
使用Control Freak创建紧密耦合的代码
Creating tightly coupled code with Control Freak
使用服务定位器请求类的依赖项
Requesting a class’s Dependencies with a Service Locator
使用Ambient Context使Volatile Dependency全局可用
Making a Volatile Dependency globally available with Ambient Context
使用Constrained Construction强制特定的构造函数签名
Forcing a particular constructor signature with Constrained Construction
Many dishes require food to be cooked in a pan with oil. If you’re not experienced with the recipe at hand, you might start heating the oil, and then turn your back to read the recipe. But once you’re done cutting the vegetables, the oil is smoking. You might think that the smoking oil means the pan is hot and ready for cooking. This is a common misconception with inexperienced cooks. When oils start to smoke, they also start to break down. This is called their smoke point. Not only do most oils taste awful once heated past their smoke point, they form harmful compounds and lose beneficial antioxidants.
In the previous chapter, we briefly compared design patterns to recipes. A pattern provides a common language we can use to succinctly discuss a complex concept. When the concept (or rather, the implementation) becomes warped, we have an anti-pattern on our hands.
Heating oil past its smoke point is a typical example of what can be considered to be a cooking anti-pattern. It’s a commonly occurring mistake. Many inexperienced cooks do this because it seems a reasonable thing to do, but loss of taste and unhealthful foods are negative consequences.
反模式或多或少是一种描述人们一再犯下的常见错误的形式化方式。在本章中,我们将描述一些与 DI 相关的常见反模式。在我们的职业生涯中,我们看到所有这些都以一种或其他形式使用,我们一直为自己应用所有这些而感到内疚。
Anti-patterns are, more or less, a formalized way of describing common mistakes that people make again and again. In this chapter, we’ll describe some common anti-patterns related to DI. During our career, we’ve seen all of them in use in one form or other, and we’ve been guilty of applying all of them ourselves.
在许多情况下,反模式代表了在应用程序中实现 DI 的真诚尝试。但由于不完全符合 DI 基础,这些实现可能会演变成弊大于利的解决方案。了解这些反模式可以让您了解在尝试第一个 DI 项目时需要注意哪些陷阱。但即使您多年来一直在应用 DI,仍然很容易出错。
In many cases, anti-patterns represent sincere attempts at implementing DI in an application. But because of not fully complying with DI fundamentals, the implementations can morph into solutions that do more harm than good. Learning about these anti-patterns can give you an idea about what traps to be aware of as you venture into your first DI projects. But even if you’ve been applying DI for years, it’s still easy to make mistakes.
可以通过将代码重构为第 4 章介绍的一种 DI 模式来修复反模式。修复每次出现的具体困难程度取决于实现的细节。对于每个反模式,我们将提供一些关于如何将其重构为更好模式的通用指导。
Anti-patterns can be fixed by refactoring the code toward one of the DI patterns introduced in chapter 4. Exactly how difficult it is to fix each occurrence depends on the details of the implementation. For each anti-pattern, we’ll supply some generalized guidance on how to refactor it toward a better pattern.
Legacy code sometimes requires drastic measures to make your code Testable. This often means taking small steps to prevent accidentally breaking a previously working application. In some cases, an anti-pattern might be the most appropriate temporary solution. Even though the application of an anti-pattern might be an improvement over the original code, it’s important to note that this doesn’t make it any less an anti-pattern; other documented and repeatable solutions exist that are proven to be more effective. The anti-patterns covered in this chapter are listed in table 5.1.
The rest of this chapter describes each anti-pattern in greater detail, presenting them in order of importance. You can read from start to finish or only read the ones you’re interested in — each has a self-contained section. If you decide to read only part of this chapter, we recommend that you read Control Freak and Service Locator.
正如构造函数注入是最重要的 DI 模式一样,Control Freak是最常出现的反模式。它有效地阻止您应用任何类型的适当的 DI,所以我们需要在我们之前关注这个反模式向其他人讲话——你也应该如此。但是因为Service Locator看起来像是在解决一个问题,所以它是最危险的。我们将在 5.2 节中解决这个问题。
Just as Constructor Injection is the most important DI pattern, Control Freak is the most frequently occurring of the anti-patterns. It effectively prevents you from applying any kind of proper DI, so we’ll need to focus on this anti-pattern before we address the others — and so should you. But because Service Locator looks like it’s solving a problem, it’s the most dangerous. We’ll address that in section 5.2.
What’s the opposite of Inversion of Control? Originally the term Inversion of Control was coined to identify the opposite of the normal state of affairs, but we can’t talk about the “Business as Usual” anti-pattern. Instead, Control Freak describes a class that won’t relinquish control of its Volatile Dependencies.
As an example, the Control Freak anti-pattern happens when you create a new instance of a Volatile Dependency by using the new keyword. The following listing demonstrates an implementation of the Control Freak anti-pattern.
public class HomeController : Controller
{
public ViewResult Index()
{
var service = new ProductService(); ①
var products = service.GetFeaturedProducts();
return this.View(products);
}
}
Every time you create a Volatile Dependency, you explicitly state that you’re going to control the lifetime of the instance and that no one else will get a chance to Intercept that particular object. Although the new keyword is a code smell when it comes to Volatile Dependencies, you don’t need to worry about using it for Stable Dependencies.2
Control Freak最明显的例子是当你不努力在你的代码中引入抽象时。在第 2 章中,当 Mary 实现她的电子商务应用程序(第 2.1 节)时,您看到了几个这样的例子。这样的做法不尝试引入 DI。但即使在开发人员听说过 DI 和可组合性的地方,也经常可以在某些变体中找到Control Freak反模式。
The most blatant example of Control Freak is when you make no effort to introduce Abstractions in your code. You saw several examples of that in chapter 2 when Mary implemented her e-commerce application (section 2.1). Such an approach makes no attempt to introduce DI. But even where developers have heard about DI and composability, the Control Freak anti-pattern can often be found in some variation.
In the next sections, we’ll show you some examples that resemble code we’ve seen used in production. In every case, the developers had the best intentions of programming to interfaces, but never understood the underlying forces and motivations.
5.1.1 例子:通过更新依赖来控制怪胎
5.1.1 Example: Control Freak through newing up Dependencies
Many developers have heard about the principle of programming to interfaces but don’t understand the deeper rationale behind it. In an attempt to do the right thing or to follow best practices, they write code that doesn’t make much sense. For example, in listing 3.9, you saw an example of a ProductService that uses an instance of the IProductRepository interface to retrieve a list of featured products. As a reminder, the following repeats the relevant code:
public IEnumerable<DiscountedProduct> GetFeaturedProducts()
{
return
from product in this.repository.GetFeaturedProducts()
select product.ApplyDiscountFor(this.userContext);
}
The salient point is that the repository member variable represents an Abstraction. In chapter 3, you saw how the repository field can be populated via Constructor Injection, but we’ve seen other, more naïve attempts. The following listing shows one such attempt.
The repository field is declared as the IProductRepository interface, so any member in the ProductService class (such as GetFeaturedProducts) programs to an interface. Although this sounds like the right thing to do, not much is gained from doing so because, at runtime, the type will always be a SqlProductRepository. There’s no way you can Intercept or change the repository variable unless you change the code and recompile. Additionally, you don’t gain much by defining a variable as an Abstraction if you hard-code it to always have a specific concrete type. Directly newing up Dependencies is one example of the Control Freak anti-pattern.
Before we get to the analysis and possible ways to address the resulting issues generated by a Control Freak, let’s look at some more examples to give you a better idea of the context and common failed attempts. In the next example, it’s apparent that the solution isn’t optimal. Most developers will attempt to refine their approach.
The most common and erroneous attempt to fix the evident problems from newing up Dependencies involves a factory of some sort. When it comes to factories, there are several options. We’ll quickly cover each of the following:
If told that she could only deal with the IProductRepositoryAbstraction, Mary Rowan (from chapter 2) would introduce a ProductRepositoryFactory that would produce the instances she needs to get. Let’s listen in as she discusses this approach with her colleague Jens. We predict that their discussion will, conveniently, cover the factory options we’ve listed.
Mary: We need an instance of IProductRepository in this ProductService class. But IProductRepository is an interface, so we can’t just create new instances of it, and our consultant says that we shouldn’t create new instances of SqlProductRepository either.
Jens: 某种工厂怎么样?
Jens: What about some sort of factory?
玛丽: 是的,我也在想同样的事情,但我不确定如何进行。我不明白它是如何解决我们的问题的。看这里 -
Mary: Yes, I was thinking the same thing, but I’m not sure how to proceed. I don’t understand how it solves our problem. Look here —
Mary 开始编写一些代码来演示她的问题。这是玛丽写的代码:
Mary starts to write some code to demonstrate her problem. This is the code that Mary writes:
public class ProductRepositoryFactory
{
public IProductRepository Create()
{
return new SqlProductRepository();
}
}
Mary: This ProductRepositoryFactory encapsulates knowledge about how to create ProductRepository instances, but it doesn’t solve the problem, because we’d have to use it in the ProductService like this:
var factory = new ProductRepositoryFactory();
this.repository = factory.Create();
See? Now we need to create a new instance of the ProductRepositoryFactory class in the ProductService, but that still hard-codes the use of SqlProductRepository. The only thing we’ve achieved is moving the problem into another class.
Jens:是的,我明白了——我们不能用抽象工厂来解决问题吗?
Jens: Yes, I see — couldn’t we solve the problem with an Abstract Factory instead?
让我们暂停 Mary 和 Jens 的讨论,评估一下发生了什么。Mary 是完全正确的,混凝土工厂类不能解决Control Freak问题,而只能解决它。它在不增加任何价值的情况下使代码更加复杂。ProductService 现在直接控制工厂的生命周期,而工厂直接控制的生命周期ProductRepository,所以你仍然无法在运行时拦截或替换 Repository 实例。
Let’s pause Mary and Jens’ discussion to evaluate what happened. Mary is entirely correct that a Concrete Factory class doesn’t solve the Control Freak issue but only moves it around. It makes the code more complex without adding any value. ProductService now directly controls the lifetime of the factory, and the factory directly controls the lifetime of ProductRepository, so you still can’t Intercept or replace the Repository instance at runtime.
很明显,混凝土工厂不会解决任何 DI 问题,而且我们从未见过它以这种方式成功使用过。Jens 关于抽象工厂的评论听起来更有希望。
It’s fairly evident that a Concrete Factory won’t solve any DI problems, and we’ve never seen it used successfully in this fashion. Jens’ comment about Abstract Factory sounds more promising.
抽象工厂
Abstract Factory
让我们继续 Mary 和 Jens 的讨论,听听 Jens 对抽象工厂的看法。
Let’s resume Mary and Jens’ discussion and hear what Jens has to say about Abstract Factory.
Jens:如果我们像这样抽象工厂会怎么样?
Jens: What if we made the factory abstract, like this?
public interface IProductRepositoryFactory
{
IProductRepository Create();
}
This means we haven’t hard-coded any references to SqlProductRepository, and we can use the factory in the ProductService to get instances of IProductRepository.
Mary:但是既然工厂是抽象的,我们如何获得它的新实例呢?
Mary: But now that the factory is abstract, how do we get a new instance of it?
Jens:我们可以创建一个返回SqlProductService实例的实现。
Jens: We can create an implementation of it that returns SqlProductService instances.
Mary:是的,但是我们如何创建它的实例呢?
Mary: Yes, but how do we create an instance of that?
Jens:我们刚刚在ProductService……哦。等待 -
Jens: We just new it up in the ProductService ... Oh. Wait —
玛丽:那会让我们回到起点。
Mary: That would put us back where we started.
Mary 和 Jens 很快意识到抽象工厂并不能改变他们的处境。他们最初的难题是他们需要一个 abstract 的实例,而现在他们需要一个 abstract 的实例。IProductRepositoryIProductRepositoryFactory
Mary and Jens quickly realize that an Abstract Factory doesn’t change their situation. Their original conundrum was that they needed an instance of the abstract IProductRepository, and now they need an instance of the abstract IProductRepositoryFactory instead.
既然 Mary 和 Jens 已经拒绝将抽象工厂作为一种可行的选择,那么一个破坏性的选择仍然存在。玛丽和詹斯即将得出结论。
Now that Mary and Jens have rejected the Abstract Factory as a viable option, one damaging option is still open. Mary and Jens are about to reach a conclusion.
静态工厂
Static Factory
让我们听听 Mary 和 Jens 决定他们认为可行的方法。
Let’s listen as Mary and Jens decide on an approach that they think will work.
玛丽:让我们做一个静态工厂。让我演示给你看:
Mary: Let’s make a Static Factory. Let me show you:
public static class ProductRepositoryFactory
{
public static IProductRepository Create()
{
return new SqlProductRepository();
}
}
既然类是静态的,我们就不需要处理如何创建它了。
Now that the class is static, we don’t need to deal with how to create it.
Mary: We could deal with this via a configuration setting that determines which type of ProductRepository to create. Like this:
public static IProductRepository Create()
{
IConfigurationRoot configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
string repositoryType = configuration["productRepository"];
switch (repositoryType)
{
case "sql": return new SqlProductRepository();
case "azure": return AzureProductRepository();
default: throw new InvalidOperationException("...");
}
}
看?这样我们就可以确定我们应该使用基于 SQL Server 的实现还是基于 Microsoft Azure 的实现,我们甚至不需要重新编译应用程序来从一个更改为另一个。
See? This way we can determine whether we should use the SQL Server–based implementation or the Microsoft Azure–based implementation, and we don’t even need to recompile the application to change from one to the other.
詹斯:酷!这就是我们要做的。那个顾问现在一定很高兴吧!
Jens: Cool! That’s what we’ll do. That consultant must be happy now!
There are several reasons why such a Static Factory doesn’t provide a satisfactory solution to the original goal of programming to interfaces. Take a look at the Dependency graph in figure 5.1.
ProductRepositoryFactory depends on both the AzureProductRepository and SqlProductRepository classes. Because ProductService directly depends on ProductRepositoryFactory, it also depends on both concrete IProductRepository implementations — recall from section 4.1.4 that dependencies are transitive.
As long as ProductService has a dependency on the static ProductRepositoryFactory, you have unsolvable design issues. If you define the static ProductRepositoryFactory in the domain layer, it means that the domain layer needs to depend on the data access layer, because ProductRepositoryFactory creates a SqlProductRepository that’s located in that layer. The data access layer, however, already depends on the domain layer because SqlProductRepository uses types and Abstractions like Product and IProductRepository from that layer. This causes a circular reference between the two projects. Additionally, if you move ProductRepositoryFactory into the data access layer, you still need a dependency from the domain layer to the data access layer because ProductService depends on ProductRepositoryFactory. This still causes a circular dependency. Figure 5.2 shows this design issue.
No matter how you move your types around, the only way to prevent these circular dependencies between projects is by creating a single project for all types. This isn’t a viable option, however, because it tightly couples the domain layer to the data access layer and disallows your data access layer from being replaced.
Mary 和 Jens没有松散耦合的IProductRepository实现,而是以紧密耦合的模块结束。更糟糕的是,工厂总是拖延所有的实现——即使是那些不需要的!如果他们托管在 Azure 上,他们仍然需要将 Commerce.SqlDataAccess.dll(例如)与他们的应用程序一起分发。
Instead of loosely coupled IProductRepository implementations, Mary and Jens end up with tightly coupled modules. Even worse, the factory always drags along all implementations — even those that aren’t needed! If they host on Azure, they still need to distribute Commerce.SqlDataAccess.dll (for example) with their application.
如果 Mary 和 Jens 需要第三种类型的IProductRepository,他们将不得不更改工厂并重新编译他们的解决方案。尽管他们的解决方案可能是可配置的,但它是不可扩展的;如果一个单独的团队,甚至公司,需要创建一个新的存储库,他们将无法访问源代码。IProductRepository用特定于测试的实现替换具体实现也是不可能的,因为这需要IProductRepository在运行时定义实例,而不是在设计时静态地在配置文件中定义。
If Mary and Jens ever need a third type of IProductRepository, they’ll have to change the factory and recompile their solution. Although their solution may be configurable, it isn’t extensible; if a separate team, or even company, needs to create a new Repository, they’ll have no options without access to the source code. It’s also impossible to replace the concrete IProductRepository implementations with test-specific implementations, because that requires defining the IProductRepository instance at runtime, instead of statically in a configuration file at design time.
In short, a Static Factory may seem to solve the problem but, in reality, only compounds it. Even in the best cases, it forces you to reference Volatile Dependencies. Another variation of this anti-pattern can be seen when overloaded constructors are used in combination with Foreign Defaults, as you’ll see in the next example.
5.1.3 示例:通过重载构造函数控制 Freak
5.1.3 Example: Control Freak through overloaded constructors
构造函数重载在许多 .NET 代码库(包括 BCL)中相当普遍。通常,许多重载为一两个成熟的构造函数提供合理的默认值,这些构造函数将所有相关参数作为输入。(这种做法称为构造函数链接.) 有时,我们会看到 DI 的其他用途。
Constructor overloads are fairly common in many .NET code bases (including the BCL). Often, the many overloads provide reasonable defaults to one or two full-blown constructors that take all relevant parameters as input. (This practice is called Constructor Chaining.) At times, we see other uses when it comes to DI.
An all-too-common anti-pattern defines a test-specific constructor overload that allows you to explicitly define a Dependency, although the production code uses a parameterless constructor. This can be detrimental when the default implementation of the Dependency represents a Foreign Default rather than a Local Default. As we explained in section 4.4.2, you typically want to supply all Volatile Dependencies using Constructor Injection — even those that could be a Local Default.
Listing 5.3ProductService with multiple constructors
private readonly IProductRepository repository;
public ProductService() ① : this(new SqlProductRepository()) ① { ① } ① public ProductService(IProductRepository repository) ② { ② if (repository == null) ② throw new ArgumentNullException("repository"); ② ② this.repository = repository; ② } ②
At first sight, this coding style might seem like the best of both worlds. It allows fake Dependencies to be supplied for the sake of unit testing; whereas, the class can still be conveniently created without having to supply its Dependencies. The following example shows this style:
By letting ProductService create the SqlProductRepositoryVolatile Dependency, you again force strong coupling between modules. Although ProductService can be reused with different IProductRepository implementations, by supplying them via the most flexible constructor overload while testing, it disables the ability to Intercept the IProductRepository instance in the application.
Now that you’ve seen a few of examples of Control Freak, we hope you have a better idea what to look for — occurrences of the new keyword next to Volatile Dependencies. This may enable you to avoid the most obvious traps. But if you need to untangle yourself from an existing occurrence of this anti-pattern, the next section will help you deal with such a task.
5.1.4控制狂分析
5.1.4 Analysis of Control Freak
Control Freak是Inversion of Control的对立面。当您直接控制Volatile Dependencies的创建时,您最终会得到紧密耦合的代码,而失去了第 1 章中概述的许多(如果不是全部)松散耦合的好处。
Control Freak is the antithesis of Inversion of Control. When you directly control the creation of Volatile Dependencies, you end up with tightly coupled code, missing many (if not all) of the benefits of loose coupling outlined in chapter 1.
Control Freak是最常见的 DI 反模式。它代表了大多数编程语言中创建实例的默认方式,因此即使在开发人员从未考虑过 DI 的应用程序中也可以观察到它。这是一种创建新对象的自然而根深蒂固的方式,许多开发人员发现很难放弃。即使当他们开始考虑 DI 时,他们也很难动摇他们必须以某种方式控制创建实例的时间和地点的心态。放开这种控制可能是一个艰难的精神飞跃;但是,即使您做到了,也有其他的陷阱需要避免,尽管这些陷阱较小。
Control Freak is the most common DI anti-pattern. It represents the default way of creating instances in most programming languages, so it can be observed even in applications where developers have never considered DI. It’s such a natural and deeply rooted way to create new objects that many developers find it difficult to discard. Even when they begin to think about DI, they have a hard time shaking the mindset that they must somehow control when and where instances are created. Letting go of that control can be a difficult mental leap to make; but, even if you make it, there are other, although lesser, pitfalls to avoid.
Control Freak反模式的负面影响
The negative effects of the Control Freak anti-pattern
With the tightly coupled code that’s the result of Control Freak, many benefits of modular design are potentially lost. These were covered in each of the previous sections, but to summarize:
Although you can configure an application to use one of multiple preconfigured Dependencies, you can’t replace them at will. It isn’t possible to provide an implementation that was created after the application was compiled, and it certainly isn’t possible to provide specific instances as an implementation.
重用使用模块变得更加困难,因为它拖拽了在新上下文中可能不需要的依赖项。例如,考虑一个模块,该模块通过使用Foreign Default依赖于 ASP.NET Core 库。这使得将该模块重用为不应或不能依赖 ASP.NET Core 的应用程序(例如,Windows 服务或手机应用程序)的一部分变得更加困难。
It becomes harder to reuse the consuming module because it drags with it Dependencies that may be undesirable in the new context. As an example of this, consider a module that, through the use of a Foreign Default, depends on ASP.NET Core libraries. This makes it harder to reuse that module as part of an application that should’t or can’t depend on ASP.NET Core (for example, a Windows Service or mobile phone application).
这使得并行开发更加困难。这是因为消费应用程序与其Dependencies的所有实现紧密耦合。
It makes parallel development more difficult. This is because the consuming application is tightly coupled to all implementations of its Dependencies.
可测试性受到影响。Test Doubles 不能用作Dependency的替代品。
Testability suffers. Test Doubles can’t be used as substitutes for the Dependency.
With careful design, you can still implement tightly coupled applications with clearly defined responsibilities so that maintainability doesn’t suffer. But even so, the cost is too high, and you’ll retain many limitations. Given the amount of effort required to accomplish that, there’s no reason to continue investing in Control Freak. You need to move away from Control Freak and toward proper DI.
从Control Freak重构到 DI
Refactoring from Control Freak toward DI
要摆脱Control Freak,您需要将代码重构为第 4 章中介绍的正确 DI 设计模式之一。作为初始步骤,您应该使用图 4.9 中给出的指导来确定目标模式。在大多数情况下,这将是构造函数注入。重构步骤如下:
To get rid of Control Freak, you need to refactor your code toward one of the proper DI design patterns presented in chapter 4. As an initial step, you should use the guidance given in figure 4.9 to determine which pattern to aim for. In most cases, this will be Constructor Injection. The refactoring steps are as follows:
Ensure that you’re programming to an Abstraction. In the examples, this was already the case; but in other situations, you may need to first extract an interface and change variable declarations.
If you create a particular implementation of a Dependency in multiple places, move them all to a single creation method. Make sure this method’s return value is expressed as the Abstraction and not the concrete type.
现在您只有一个地方可以创建实例,通过实现其中一种 DI 模式(例如构造函数注入)将此创建移出使用类。
Now that you have only a single place where you create the instance, move this creation out of the consuming class by implementing one of the DI patterns, such as Constructor Injection.
ProductService对于前面几节中的示例,构造函数注入是一个很好的解决方案。
In the case of the ProductService examples in the previous sections, Constructor Injection is an excellent solution.
Listing 5.4 Refactoring away from Control Freak using Constructor Injection
public class ProductService : IProductService
{
private readonly IProductRepository repository;
public ProductService(IProductRepository repository)
{
if (repository == null)
throw new ArgumentNullException("repository");
this.repository = repository;
}
}
Control Freak是迄今为止最具破坏性的反模式,但即使您控制了它,也会出现更微妙的问题。下一节将介绍更多反模式。尽管它们比Control Freak问题更少,但它们也往往更容易解决,因此请多加留意,并在发现它们时进行修复。
Control Freak is by far the most damaging anti-pattern, but even when you have it under control, more subtle issues can arise. The next sections look at more anti-patterns. Although they’re less problematic than Control Freak, they also tend to be easier to resolve, so be on the lookout, and fix them as they’re discovered.
It can be difficult to give up on the idea of directly controlling Dependencies, so many developers take Static Factories (such as the one described in section 5.1.2) to new levels. This leads to the Service Locator anti-pattern.
As it’s most commonly implemented, the Service Locator is a Static Factory that can be configured with concrete services before the first consumer begins to use it. (But you’ll equally also find abstract Service Locators.) This could conceivably happen in the Composition Root. Depending on the particular implementation, the Service Locator can be configured with code by reading a configuration file or by using a combination thereof. The following listing shows the Service Locator anti-pattern in action.
Listing 5.5 Using the Service Locator anti-pattern
public class HomeController : Controller
{
public HomeController() { } ①
public ViewResult Index()
{
IProductService service =
Locator.GetService<IProductService>(); ② var products = service.GetFeaturedProducts(); ③
return this.View(products);
}
}
Instead of statically defining the list of required Dependencies, HomeController has a parameterless constructor, requesting its Dependencies later. This hides these Dependencies from HomeController’s consumers and makes HomeController harder to use and test. Figure 5.3 shows the interaction in listing 5.5, where you can see the relationship between the Service Locator and the ProductService implementation.
Years ago, it was quite controversial to call Service Locator an anti-pattern. The controversy is over: Service Locator is an anti-pattern. But don’t be surprised to find code bases that have this anti-pattern sprinkled all over the place.
It’s important to note that if you look at only the static structure of classes, a DI Container looks like a Service Locator. The difference is subtle and lies not in the mechanics of implementation, but in how you use it. In essence, asking a container or locator to resolve a complete object graph from the Composition Root is proper usage. Asking it for granular services from anywhere else but the Composition Root implies the Service Locator anti-pattern. Let’s review an example that shows Service Locator in action.
5.2.1 示例:ProductService使用服务定位器
5.2.1 Example: ProductService using a Service Locator
Let’s return to our tried-and-tested ProductService, which requires an instance of the IProductRepository interface. Assuming we were to apply the Service Locator anti-pattern, ProductService would use the static GetService method, as shown in the following listing.
In this example, we implement the GetService method using generic type parameters to indicate the type of service being requested. You could also use a Type argument to indicate the type, if that’s more to your liking.
As the following listing shows, this implementation of the Locator class is as short as possible. We could have added Guard Clauses and error handling, but we wanted to highlight the core behavior. The code could also include a feature that enables it to load its configuration from a file, but we’ll leave that as an exercise for you.
Listing 5.7 A simple Service Locator implementation
public static class Locator
{
private static Dictionary<Type, object> services = ①
new Dictionary<Type, object>();
public static void Register<T>(T service)
{
services[typeof(T)] = service;
}
public static T GetService<T>() ② { ② return (T)services[typeof(T)]; ②
}
public static void Reset()
{
services.Clear();
}
}
Clients such as ProductService can use the GetService method to request an instance of the abstract type T. Because this example code contains no Guard Clauses or error handling, the method throws a rather cryptic KeyNotFoundException if the requested type has no entry in the dictionary. You can imagine how to add code to throw a more descriptive exception.
The GetService method can only return an instance of the requested type if it has previously been inserted in the internal dictionary. This can be done with the Register method. Again, this example code contains no Guard Clause, so it would be possible to Register a null value, but a more robust implementation shouldn’t allow that. This implementation also caches registered instances forever, but it isn’t that hard to come up with an implementation that allows creating new instances on every call to GetService. In certain cases, particularly when unit testing, it’s important to be able to reset the Service Locator. That functionality is provided by the Reset method, which clears the internal dictionary.
Classes like ProductService rely on the service to be available in the Service Locator, so it’s important that it’s previously configured. In a unit test, this could be done with a Test Double implemented by a Stub, as can be seen in the following listing.7
Listing 5.8 A unit test depending on a Service Locator
[Fact]
public void GetFeaturedProductsWillReturnInstance()
{
// Arrange
var stub = ProductRepositoryStub(); ① Locator.Reset(); ② Locator.Register<IProductRepository>(stub); ③
var sut = new ProductService();
// Act
var result = sut.GetFeaturedProducts(); ④
// Assert
Assert.NotNull(result);
}
The example shows how the static Register method is used to configure the Service Locator with the Stub instance. If this is done before ProductService is constructed, as shown in the example, ProductService uses the configured Stub to work against ProductRepository. In the full production application, the Service Locator will be configured with the correct ProductRepository implementation in the Composition Root.
This way of locating Dependencies from the ProductService class definitely works if our only success criterion is that the Dependency can be used and replaced at will. But it has some serious shortcomings.
Service Locator is a dangerous pattern because it almost works. You can locate Dependencies from consuming classes, and you can replace those Dependencies with different implementations — even with Test Doubles from unit tests. When you apply the analysis model outlined in chapter 1 to evaluate whether Service Locator can match the benefits of modular application design, you’ll find that it fits in most regards:
您可以通过更改注册来支持后期绑定。
You can support late binding by changing the registration.
您可以并行开发代码,因为您是针对接口进行编程,可以随意替换模块。
You can develop code in parallel, because you’re programming against interfaces, replacing modules at will.
您可以实现良好的关注点分离,因此没有什么能阻止您编写可维护的代码,但这样做会变得更加困难。
You can achieve good separation of concerns, so nothing stops you from writing maintainable code, but doing so becomes more difficult.
您可以将依赖项替换为测试替身,从而确保可测试性。
You can replace Dependencies with Test Doubles, so Testability is ensured.
Service Locator仅在一个方面存在不足,因此不应掉以轻心。
There’s only one area where Service Locator falls short, and that shouldn’t be taken lightly.
服务定位器反模式的负面影响
Negative effects of the Service Locator anti-pattern
Service Locator的主要问题是它会影响使用它的类的可重用性。这表现在两个方面:
The main problem with Service Locator is that it impacts the reusability of the classes consuming it. This manifests itself in two ways:
该类作为冗余Dependency沿Service Locator拖动。
The class drags along the Service Locator as a redundant Dependency.
该类使其依赖项不明显。
The class makes it non-obvious what its Dependencies are.
In addition to the expected reference to IProductRepository, ProductService also depends on the Locator class. This means that to reuse the ProductService class, you must redistribute not only it and its relevant DependencyIProductRepository, but also the LocatorDependency, which only exists for mechanical reasons. If the Locator class is defined in a different module than ProductService and IProductRepository, new applications wanting to reuse ProductService must accept that module too.
图 5.5 Visual Studio 的 IntelliSense 唯一可以告诉我们关于ProductService类的事情是它有一个无参数的构造函数。它的依赖项是不可见的。
Figure 5.5 The only thing Visual Studio’s IntelliSense can tell us about the ProductService class is that it has a parameterless constructor. Its Dependencies are invisible.
也许我们甚至可以容忍这种额外的依赖关系是否Locator真的有必要让 DI 工作。我们会接受它作为为获得其他利益而支付的税款。但是有更好的选择(比如Constructor Injection)可用,所以这个Dependency是多余的。此外,对于想要使用该类的开发人员来说,这种冗余的依赖项和它的相关对应项都不是明确可见的。图 5.5显示 Visual Studio 没有提供有关使用此类的指导。IProductRepositoryProductService
Perhaps we could even tolerate that extra Dependency on Locator if it was truly necessary for DI to work. We’d accept it as a tax to be paid to gain other benefits. But there are better options (such as Constructor Injection) available, so this Dependency is redundant. Moreover, neither this redundant Dependency nor IProductRepository, its relevant counterpart, is explicitly visible to developers wanting to consume the ProductService class. Figure 5.5 shows that Visual Studio offers no guidance on the use of this class.
如果你想创建一个ProductService类的新实例, Visual Studio 只能告诉您该类具有无参数构造函数。但是如果您随后尝试运行代码,如果您忘记向该类注册IProductRepository实例,则会出现运行时错误Locator. 如果您不熟悉该ProductService课程,则很可能会发生这种情况。
If you want to create a new instance of the ProductService class, Visual Studio can only tell you that the class has a parameterless constructor. But if you subsequently attempt to run the code, you get a runtime error if you forgot to register an IProductRepository instance with the Locator class. This is likely to happen if you don’t intimately know the ProductService class.
The ProductService class is far from self documenting: you can’t tell which Dependencies must be present before it’ll work. In fact, the developers of ProductService may even decide to add more Dependencies in future versions. That would mean that code that works for the current version can fail in a future version, and you aren’t going to get a compiler error that warns you. Service Locator makes it easy to inadvertently introduce breaking changes.
The use of generics may trick you into thinking that a Service Locator is strongly typed. But even an API like the one shown in listing 5.7 is weakly typed, because you can request any type. Being able to compile code invoking the GetService<T> method gives you no guarantee that it won’t throw exceptions left and right at runtime.
When unit testing, you have the additional problem that a Test Double registered in one test case will lead to the Interdependent Tests code smell, because it remains in memory when the next test case is executed. It’s therefore necessary to perform Fixture Teardown after every test by invoking Locator.Reset().8 This is something that you must manually remember to do, and it’s easy to forget.
服务定位器可能看起来无害,但它可能导致各种令人讨厌的运行时错误。你如何避免这些问题?当您决定摆脱Service Locator时,您需要找到一种方法来做到这一点。一如既往,默认方法应该是构造函数注入,除非第 4 章中的其他 DI 模式之一提供了更好的匹配。
A Service Locator may seem innocuous, but it can lead to all sorts of nasty runtime errors. How do you avoid those problems? When you decide to get rid of a Service Locator, you need to find a way to do it. As always, the default approach should be Constructor Injection, unless one of the other DI patterns from chapter 4 provides a better fit.
Because Constructor Injection statically declares a class’s Dependencies, it enables the code to fail at compile time, assuming you practice Pure DI. When you use a DI Container, on the other hand, you lose the ability to verify correctness at compile time. Statically declaring a class’s Dependencies, however, still ensures that you can verify the correctness of your application’s object graphs by asking the container to create all object graphs for you. You can do this at application startup or as part of a unit/integration test.
一些DI 容器甚至更进一步,允许对 DI 配置进行更复杂的分析。这允许检测各种常见的陷阱。另一方面,Service Locator对DI Container是完全不可见的,因此它不可能代表您进行此类验证。
Some DI Containers even take this a step further and allow doing more-complex analysis on the DI configuration. This allows detecting all kinds of common pitfalls. A Service Locator, on the other hand, will be completely invisible to a DI Container, making it impossible for it to do these kinds of verification on your behalf.
In many cases, a class that consumes a Service Locator may have calls to it spread throughout its code base. In such cases, it acts as a replacement for the new statement. When this is so, the first refactoring step is to consolidate the creation of each Dependency in a single method.
If you don’t have a member field to hold an instance of the Dependency, you can introduce such a field and make sure the rest of the code uses this field when it consumes the Dependency. Mark the field readonly to ensure that it can’t be modified outside the constructor. Doing so forces you to assign the field from the constructor using the Service Locator. You can now introduce a constructor parameter that assigns the field instead of the Service Locator, which can then be removed.
重构使用服务定位器的类类似于重构使用Control Freak的类。第 5.1.4 节包含有关重构Control Freak实现以使用 DI 的进一步说明。
Refactoring a class that uses Service Locator is similar to refactoring a class that uses Control Freak. Section 5.1.4 contains further notes on refactoring Control Freak implementations to use DI.
乍一看,Service Locator可能看起来像是一种合适的 DI 模式,但不要被愚弄:它可能明确地解决了松耦合问题,但同时也牺牲了其他问题。第 4 章中介绍的 DI 模式提供了更好的替代方案,缺点更少。服务定位器反模式以及本章介绍的其他反模式都是如此。尽管它们不同,但它们都有一个共同特征,即它们可以通过第 4 章中的一种 DI 模式来解决。
At first glance, Service Locator may look like a proper DI pattern, but don’t be fooled: it may explicitly address loose coupling, but it sacrifices other concerns along the way. The DI patterns presented in chapter 4 offer better alternatives with fewer drawbacks. This is true for the Service Locator anti-pattern, as well as the other anti-patterns presented in this chapter. Even though they’re different, they all share the common trait that they can be resolved by one of the DI patterns from chapter 4.
Related to Service Locator is the Ambient Context anti-pattern. Where a Service Locator allows global access to an unrestricted set of Dependencies, an Ambient Context makes a single strongly typed Dependency available through a static accessor.
以下清单显示了Ambient Context反模式的实际应用。
The following listing shows the Ambient Context anti-pattern in action.
In this example, ITimeProvider presents an Abstraction that allows retrieving the system’s current time. Because you might want to influence how time is perceived by the application (for instance, for testing), you don’t want to call DateTime.Now directly. Instead of letting consumers call DateTime.Now directly, a good solution is to hide access to DateTime.Now behind an Abstraction. It’s all too tempting, however, to allow consumers to access the default implementation through a static property or method. In listing 5.9, the Current property allows access to the default ITimeProvider implementation.
Ambient Context is similar in structure to the Singleton pattern.9 Both allow access to a Dependency by the use of static class members. The difference is that Ambient Context allows its Dependency to be changed, whereas the Singleton pattern ensures that its singular instance never changes.
访问系统的当前时间是一种常见的需求。让我们更深入地研究这个ITimeProvider例子。
The access to the system’s current time is a common need. Let’s dive a little bit deeper into the ITimeProvider example.
5.3.1 示例:通过环境上下文访问时间
5.3.1 Example: Accessing time through Ambient Context
There are many reasons one would need to exercise some control over time. Many applications have business logic that depends on time or the progression of it. In the previous example, you saw a simple case where we displayed a welcome message based on the current time. Two other examples include these:
基于星期几的成本计算。在某些企业中,客户在周末为服务支付更多费用是正常的。
Cost calculations based on day of the week. In some businesses, it’s normal for customers to pay more for services during the weekend.
Sending notifications to users using different communication channels based on the time of day. For instance, the business might want email notifications to be sent during working hours, and by text message or pager, otherwise.
Because the need to work with time is such a widespread requirement, developers often feel the urge to simplify access to such a Volatile Dependency by using an Ambient Context. The following listing shows an example ITimeProviderAbstraction.
Listing 5.11 A TimeProviderAmbient Context implementation
public static class TimeProvider ①
{
private static ITimeProvider current =
new DefaultTimeProvider(); ② public static ITimeProvider Current ③
{
get { return current; }
set { current = value; }
}
private class DefaultTimeProvider : ITimeProvider ④
{
public DateTime Now { get { return DateTime.Now; } }
}
}
Listing 5.12 A unit test depending on an Ambient Context
[Fact]
public void SaysGoodDayDuringDayTime()
{
// Arrange
DateTime dayTime = DateTime.Parse("2019-01-01 6:00");
var stub = new TimeProviderStub { Now = dayTime };
TimeProvider.Current = stub; ① var sut = new WelcomeMessageGenerator(); ②
// Act
string actualMessage = sut.GetWelcomeMessage(); ③
// Assert
Assert.Equal(expected: "Good day.", actual: actualMessage);
}
这是环境上下文反模式的一种变体。您可能遇到的其他常见变体是:
This is one variation of the Ambient Context anti-pattern. Other common variations you might encounter are these:
An Ambient Context that allows consumers to make use of the behavior of a globally configured Dependency. With the previous example in mind, the TimeProvider could supply consumers with a static GetCurrentTime method that hides the used Dependency by calling it internally.
An Ambient Context that merges the static accessor with the interface into a single Abstraction. In respect to the previous example, that would mean that you have a single TimeProvider base class that contains both the Now instance property and the static Current property.
An Ambient Context where delegates are used instead of a custom-defined Abstraction. Instead of having a fairly descriptive ITimeProvider interface, you could achieve the same using a Func<DateTime> delegate.
Ambient Context can come in many shapes and implementations. Again, the caution regarding Ambient Context is that it provides either direct or indirect access to a Volatile Dependency by means of some static class member. Before doing the analysis and evaluating possible ways to fix the problems caused by Ambient Context, let’s look at another common example of Ambient Context.
5.3.2 示例:通过环境上下文记录
5.3.2 Example: Logging through Ambient Context
开发人员倾向于走捷径并步入环境上下文陷阱的另一个常见情况是在将日志记录应用于他们的应用程序时。任何真实的应用程序都需要能够将有关错误和其他不常见情况的信息写入文件或其他来源以供以后分析。许多开发人员认为日志记录是一项特殊的活动,值得“打破规则”。即使在非常熟悉 DI 的开发人员的代码库中,您也可能会发现类似于下一个清单中所示的代码。
Another common case where developers tend to take a shortcut and step into the Ambient Context trap is when it comes to applying logging to their applications. Any real application requires the ability to write information about errors and other uncommon conditions to a file or other source for later analysis. Many developers feel that logging is such a special activity that it deserves “bending the rules.” You might find code similar to that shown in the next listing even in the code bases of developers who are quite familiar with DI.
public class MessageGenerator
{
private static readonly ILog Logger =
LogManager.GetLogger(typeof(MessageGenerator)); ①
public string GetWelcomeMessage()
{
Logger.Info("GetWelcomeMessage called."); ②
return string.Format(
"Hello. Current time is: {0}.", DateTime.Now);
}
}
There are several reasons why Ambient Context is so ubiquitous in many applications when it comes to logging. First, code like listing 5.13 is typically the first example that logging libraries show in their documentation. Developers copy those examples out of ignorance. We can’t blame them; developers typically assume that the library designers know and communicate best practices. Unfortunately, this isn’t always the case. Documentation examples are typically written for simplicity, not best practice, even if their designers understand those best practices.
Apart from that, developers tend to apply Ambient Context for loggers because they need logging in almost every class in their application. Injecting it in the constructor could easily lead to constructors with too many Dependencies. This is indeed a code smell called Constructor Over-injection, and we’ll discuss it in chapter 6.
When working on Stack Overflow, Jeff removed most of the logging, relying exclusively on logging of unhandled exceptions. If it’s an error, an exception should be thrown.
我们完全同意 Jeff 的分析,但也想从设计的角度来解决这个问题。我们发现,通过良好的应用程序设计,您将能够跨公共组件应用日志记录,而不会污染您的整个代码库。第 10 章详细描述了如何设计这样的应用程序。
We wholeheartedly agree with Jeff’s analysis, but would also like to approach this from a design perspective. We’ve found that with good application design, you’ll be able to apply logging across common components, without having it pollute your entire code base. Chapter 10 describes in detail how to design such an application.
There are many other examples of Ambient Context, but these two examples are so common and widespread that we’ve seen them countless times in companies we’ve consulted with. (We’ve even been guilty of introducing Ambient Context implementations ourselves in the past.) Now that you’ve seen the two most common examples of Ambient Context, the next section discusses why it’s a problem and how to deal with it.
Ambient Context is usually encountered when developers have a Cross-Cutting Concern as a Volatile Dependency, which is used ubiquitously. This ubiquitous nature makes developers think it justifies moving away from Constructor Injection. It allows them to hide Dependencies and avoids the necessity of adding the Dependency to many constructors in their application.
环境上下文反模式的负面影响
Negative effects of the Ambient Context anti-pattern
Ambient Context的问题与Service Locator的问题有关。以下是主要问题:
The problems with Ambient Context are related to the problems with Service Locator. Here are the main issues:
依赖项是隐藏的。
The Dependency is hidden.
测试变得更加困难。
Testing becomes more difficult.
很难根据上下文更改依赖项。
It becomes hard to change the Dependency based on its context.
Dependency的初始化和使用之间存在Temporal Coupling。
There’s Temporal Coupling between the initialization of the Dependency and its usage.
When you hide a Dependency by allowing global access to it through Ambient Context, it becomes easier to hide the fact that a class has too many Dependencies. This is related to the Constructor Over-injection code smell and is typically an indication that you’re violating the Single Responsibility Principle.
When a class has many Dependencies, it’s an indication that it’s doing more than it should. It’s theoretically possible to have a class with many Dependencies, while still having just “one reason to change.”11 The larger the class, however, the less likely it is to abide by this guidance. The use of Ambient Context hides the fact that classes might have become too complex, and need to be refactored.
Ambient Context also makes testing more difficult because it presents a global state. When a test changes the global state, as you saw in listing 5.12, it might influence other tests. This is the case when tests run in parallel, but even sequentially executed tests can be affected when a test forgets to revert its changes as part of its teardown. Although these test-related issues can be mitigated, it means building a specially crafted Ambient Context and either global or test-specific teardown logic. This adds complexity, whereas the alternative doesn’t.
The use of an Ambient Context makes it hard to provide different consumers with different implementations of the Dependency. For instance, say you need part of your system to work with a moment in time that’s fixed at the start of the current request, whereas other, possibly long-running operations, should get a Dependency that’s live-updated.12 Providing consumers with different implementations of the Dependency is exactly what happened in listing 5.13, as repeated here:
To be able to provide consumers with different implementations, the GetLogger API requires the consumer to pass along its appropriate type information. This needlessly complicates the consumer.
The use of an Ambient Context causes the usage of its Dependency coupled on a temporal level. Unless you initialize the Ambient Context in the Composition Root, the application fails when the class starts using the Dependency for the first time. We rather want our applications to fail fast instead.
Although Ambient Context isn’t as destructive as Service Locator, because it only hides a single Volatile Dependency opposed to an arbitrary number of Dependencies, it has no place in a well-designed code base. There are always better alternatives, which is what we describe in the next section.
从环境上下文重构到 DI
Refactoring from Ambient Context toward DI
即使在开发人员相当了解 DI 和服务定位器带来的危害的代码库中,看到Ambient Context也不要感到惊讶。很难说服开发人员放弃Ambient Context,因为他们已经习惯使用它。最重要的是,尽管针对 DI 重构单个类并不难,但诸如无效和有害的日志记录策略等潜在问题更难改变。通常,有很多代码出于并不总是很清楚的原因进行记录。当原始开发人员早已不在时,找出是否可以删除这些日志记录语句或者是否应该将其转化为异常通常是一个缓慢的过程。不过,假设代码库已经应用了 DI,从Ambient Context重构到 DI 是直截了当的。
Don’t be surprised to see Ambient Context even in code bases where the developers have a fairly good understanding of DI and the harm that Service Locator brings. It can be hard to convince developers to move away from Ambient Context, because they’re so accustomed to using it. On top of that, although refactoring a single class toward DI isn’t hard, the underlying problems like ineffective and harmful logging strategies are harder to change. Typically, there’s lots of code that logs for reasons that aren’t always clear. Finding out whether these logging statements could be removed or should be turned into exceptions instead can often be a slow process when the original developers are long gone. Still, assuming a code base already applies DI, refactoring away from Ambient Context toward DI is straightforward.
A class that consumes an Ambient Context typically contains one or a few calls to it, possibly spread over multiple methods. Because the first refactoring step is to centralize the call to the Ambient Context, the constructor is a good place to do this.
Create a private readonly field that can hold a reference to the Dependency and assign it with the Ambient Context’s Dependency. The rest of the class’s code can now use this new private field. The call to the Ambient Context can now be replaced with a constructor parameter that assigns the field and a Guard Clause that ensures the constructor parameter isn’t null. This new constructor parameter will likely cause consumers to break. But if DI was applied already, this should only cause changes to the Composition Root and the class’s tests. The following listing shows the (unsurprising) result of the refactoring, when applied to the WelcomeMessageGenerator.
Listing 5.14 Refactoring away from Ambient Context to Constructor Injection
public class WelcomeMessageGenerator
{
private readonly ITimeProvider timeProvider;
public WelcomeMessageGenerator(ITimeProvider timeProvider)
{
if (timeProvider == null)
throw new ArgumentNullException("timeProvider");
this.timeProvider = timeProvider;
}
public string GetWelcomeMessage()
{
DateTime now = this.timeProvider.Now;
...
}
}
重构Ambient Context相对简单,因为在大多数情况下,您将在已经应用 DI 的应用程序中进行重构。对于不支持的应用程序,最好先解决Control Freak和服务定位器问题,然后再处理环境上下文重构。
Refactoring Ambient Context is relatively simple because, for the most part, you’ll be doing it in an application that has already applied DI. For applications that don’t, it’s better to fix Control Freak and Service Locator problems first before tackling Ambient Context refactorings.
Ambient Context sounds like a great way to access commonly used Cross-Cutting Concerns, but looks are deceiving. Although less problematic than Control Freak and Service Locator, Ambient Context is typically a cover-up for larger design problems in the application. The patterns described in chapter 4 provide a better solution, and in chapter 10, we’ll show how to design your applications in such way that logging and other Cross-Cutting Concerns can be applied more easily and transparently across the application.
The last anti-pattern considered in this chapter is Constrained Construction. This often originates from the desire to attain late binding.
5.4 约束构造
5.4 Constrained Construction
正确实施 DI 的最大挑战是将所有具有Dependencies的类移动到Composition Root。当你做到这一点时,你已经走了很长一段路。尽管如此,仍有一些陷阱需要注意。
The biggest challenge of properly implementing DI is getting all classes with Dependencies moved to a Composition Root. When you accomplish this, you’ve already come a long way. Even so, there are still some traps to look out for.
A common mistake is to require Dependencies to have a constructor with a particular signature. This normally originates from the desire to attain late binding so that Dependencies can be defined in an external configuration file and thereby changed without recompiling the application.
Be aware that this section applies only to scenarios where late binding is desired. In scenarios where you directly reference all Dependencies from the application’s root, you won’t have this problem. But then again, you won’t have the ability to replace Dependencies without recompiling the startup project, either. The following listing shows the Constrained Construction anti-pattern in action.
Listing 5.15Constrained Construction anti-pattern example
public class SqlProductRepository : IProductRepository
{
public SqlProductRepository(string connectionStr) ①
{
}
}
public class AzureProductRepository : IProductRepository
{
public AzureProductRepository(string connectionStr) ①
{
}
}
IProductRepository抽象的所有实现都必须具有具有相同签名的构造函数。在此示例中,构造函数应该只有一个 type 参数。尽管一个类具有type的Dependency是完全没问题的,但是强制这些实现具有相同的构造函数签名是一个问题。在 1.2.2 节中,我们简要地谈到了这个问题。本节对其进行更仔细的检查。stringstring
All implementations of the IProductRepositoryAbstraction are forced to have a constructor with the same signature. In this example, the constructor should have exactly one argument of type string. Although it’s perfectly fine for a class to have a Dependency of type string, it’s a problem for those implementations to be forced to have an identical constructor signature. In section 1.2.2, we briefly touched on this issue. This section examines it more carefully.
5.4.1 示例:后期绑定 ProductRepository
5.4.1 Example: Late binding a ProductRepository
在示例电子商务应用程序中,一些类依赖于IProductRepository接口. 这意味着要创建这些类,您首先需要创建一个IProductRepository实现。此时,您已经了解到组合根是执行此操作的正确位置。在 ASP.NET Core 应用程序中,这通常意味着Startup. 以下清单显示了创建IProductRepository.
In the sample e-commerce application, some classes depend on the IProductRepository interface. This means that to create those classes, you first need to create an IProductRepository implementation. At this point, you’ve learned that a Composition Root is the correct place to do this. In an ASP.NET Core application, this typically means Startup. The following listing shows the relevant part that creates an instance of an IProductRepository.
Listing 5.16 Implicitly constraining the ProductRepository constructor
string connectionString = this.Configuration ① .GetConnectionString("CommerceConnectionString"); ① var settings = ② this.Configuration.GetSection("AppSettings"); ② ② string productRepositoryTypeName = ② settings.GetValue<string>("ProductRepositoryType"); ② var productRepositoryType = ③ Type.GetType( ③ typeName: productRepositoryTypeName, ③ throwOnError: true); ③
var constructorArguments =
new object[] { connectionString };
IProductRepository repository = ④ (IProductRepository)Activator.CreateInstance( ④ productRepositoryType, constructorArguments); ④
以下代码显示了相应的配置文件:
The following code shows the corresponding configuration file:
The first thing that should trigger suspicion is that a connection string is read from the configuration file. Why do you need a connection string if you plan to treat a ProductRepository as an Abstraction?
虽然这不太可能,但您可以选择ProductRepository使用内存数据库或 XML 文件来实现。基于 REST 的存储服务,如 Windows Azure 表存储服务,提供了一个更现实的选择,尽管今年最受欢迎的选择似乎还是关系数据库。数据库的无处不在使得人们很容易忘记连接字符串隐式表示实现选择。
Although it’s perhaps a bit unlikely, you could choose to implement a ProductRepository with an in-memory database or an XML file. A REST-based storage service, such as the Windows Azure Table Storage Service, offers a more realistic alternative, although, once again this year, the most popular choice seems to be a relational database. The ubiquity of databases makes it all too easy to forget that a connection string implicitly represents an implementation choice.
要后期绑定IProductRepository,您还需要确定选择了哪种类型作为实现。这可以通过从配置中读取程序集限定类型名称并Type从该名称创建实例来完成。这本身没有问题。当您需要创建该类型的实例时,困难就出现了。给定 a ,您可以使用该类Type创建一个实例Activator. 这创建实例方法调用类型的构造函数,因此您必须提供正确的构造函数参数以防止抛出异常。在这种情况下,您提供一个连接字符串。
To late bind an IProductRepository, you also need to determine which type has been chosen as the implementation. This can be done by reading an assembly-qualified type name from the configuration and creating a Type instance from that name. This in itself isn’t problematic. The difficulty arises when you need to create an instance of that type. Given a Type, you can create an instance using the Activator class. The CreateInstance method invokes the type’s constructor, so you must supply the correct constructor parameters to prevent an exception from being thrown. In this case, you supply a connection string.
如果您除了清单 5.16中的代码之外对该应用程序一无所知,您现在应该想知道为什么连接字符串作为构造函数参数传递给未知类型。如果实现是基于基于 REST 的 Web 服务或 XML 文件,那将毫无意义。
If you didn’t know anything else about the application other than the code in listing 5.16, you should by now be wondering why a connection string is passed as a constructor argument to an unknown type. It wouldn’t make sense if the implementation was based on a REST-based web service or an XML file.
Indeed, it doesn’t make sense because this represents an accidental constraint on the Dependency’s constructor. In this case, you have an implicit requirement that any implementation of IProductRepository should have a constructor that takes a single string as input. This is in addition to the explicit constraint that the class must derive from IProductRepository.
您可能会争辩说,IProductRepository基于 XML 文件的应用程序还需要一个字符串作为构造函数参数,尽管该字符串是文件名而不是连接字符串。但是,从概念上讲,它仍然很奇怪,因为您必须在connectionStrings配置元素中定义该文件名。(无论如何,我们认为这样的假设应该将 an作为构造函数参数而不是文件名。)XmlProductRepositoryXmlReader
You could argue that an IProductRepository based on an XML file would also require a string as constructor parameter, although that string would be a filename and not a connection string. But, conceptually, it’d still be weird because you’d have to define that filename in the connectionStrings element of the configuration. (In any case, we think such a hypothetical XmlProductRepository should take an XmlReader as a constructor argument instead of a filename.)
In the previous example, the implicit constraint required implementers to have a constructor with a single string parameter. A more common constraint is that all implementations should have a parameterless constructor, so that the simplest form of Activator.CreateInstance will work:
Although this can be said to be the lowest common denominator, the cost in flexibility is significant. No matter how you constrain object construction, you lose flexibility.
Constrained Construction反模式的负面影响
Negative effects of the Constrained Construction anti-pattern
It might be tempting to declare that all Dependency implementations should have a parameterless constructor. After all, they could perform their initialization internally; for example, reading configuration data like connection strings directly from the configuration file. But this would limit you in other ways because you might want to compose an application as layers of instances that encapsulate other instances. In some cases, for example, you might want to share an instance between different consumers, as illustrated in figure 5.6.
When you have more than one class requiring the same Dependency, you may want to share a single instance among all those classes. This is possible only when you can inject that instance from the outside. Although you could write code inside each of those classes to read type information from a configuration file and use Activator.CreateInstance to create the correct type of instance, it’d be really involved to share a single instance this way. Instead, you’d have multiple instances of the same class taking up more memory.
Instead of imposing implicit constraints on how objects should be constructed, you should implement your Composition Root so that it can deal with any kind of constructor or factory method you may throw at it. Now let’s take a look at how you can refactor toward DI.
从约束构造重构到 DI
Refactoring from Constrained Construction toward DI
How can you deal with having no constraints on components’ constructors when you need late binding? It may be tempting to introduce an Abstract Factory that can create instances of the required Abstraction and then require the implementations of those Abstract Factories to have a particular constructor signature. But doing so, however, is likely to cause complications of its own. Let’s examine such an approach.
Imagine using an Abstract Factory for the IProductRepositoryAbstraction. The Abstract Factory scheme dictates that you also need an IProductRepositoryFactory interface. Figure 5.7 illustrates this structure.
In this figure, IProductRepository represents the real Dependency. But to keep its implementers free of implicit constraints, you attempt to solve the late-binding challenge by introducing an IProductRepositoryFactory. This will be used to create instances of IProductRepository. A further requirement is that any factories have a particular constructor signature.
Now let’s assume that you want to use an implementation of IProductRepository that requires an instance of IUserContext to work, as shown in the next listing.
The SqlProductRepository class implements the IProductRepository interface, but requires an instance of IUserContext. Because the only constructor isn’t a parameterless constructor, IProductRepositoryFactory will come in handy.
当前,您希望使用IUserContext基于 ASP.NET Core 的实现。你称这个实现为(正如我们在代码清单 3.12 中所讨论的)。因为实现依赖于 ASP.NET Core,所以它没有在与. 而且,因为您不想将对包含的库的引用与 一起拖动,所以唯一的解决方案是在与 不同的程序集中实现,如图 5.8所示。AspNetUserContextAdapterSqlProductRepositoryAspNetUserContextAdapterSqlProductRepositorySqlProductRepositoryFactorySqlProductRepository
Currently, you want to use an implementation of IUserContext that’s based on ASP.NET Core. You call this implementation AspNetUserContextAdapter (as we discussed in listing 3.12). Because the implementation depends on ASP.NET Core, it isn’t defined in the same assembly as SqlProductRepository. And, because you don’t want to drag a reference to the library that contains AspNetUserContextAdapter along with SqlProductRepository, the only solution is to implement SqlProductRepositoryFactory in a different assembly than SqlProductRepository, as shown in figure 5.8.
Listing 5.18 Factory that creates SqlProductRepository instances
public class SqlProductRepositoryFactory
: IProductRepositoryFactory
{
private readonly string connectionString;
public SqlProductRepositoryFactory(
IConfigurationRoot configuration) ①
{
this.connectionString =
configuration.GetConnectionString( ②
"CommerceConnectionString");
}
public IProductRepository Create()
{
return new SqlProductRepository( ③
new AspNetUserContextAdapter(),
new CommerceContext(this.connectionString));
}
}
Even though IProductRepository and IProductRepositoryFactory look like a cohesive pair, it’s important to implement them in two different assemblies. This is because the factory must have references to all Dependencies to be able to wire them together correctly. By convention, the IProductRepositoryFactory implementation must again use Constrained Construction so that you can write the assembly-qualified type name in a configuration file and use Activator.CreateInstance to create an instance.
Every time you need to wire together a new combination of Dependencies, you must implement a new factory that wires up exactly that combination, and then configure the application to use that factory instead of the previous one. This means you can’t define arbitrary combinations of Dependencies without writing and compiling code, but you can do it without recompiling the application itself. Such an Abstract Factory becomes an Abstract Composition Root that’s defined in an assembly separate from the core application. Although this is possible, when you try to apply it, you’ll notice the inflexibility that it causes.
Flexibility suffers because the Abstract Composition Root takes direct dependencies on concrete types in other libraries to fulfill the needs of the object graphs it builds. In the SqlProductRepositoryFactory example, the factory needs to create an instance of AspNetUserContextAdapter to pass to SqlProductRepository. But what if the core application wants to replace or Intercept the IUserContext implementation? This forces changes to both the core application and the SqlProductRepositoryFactory project. Another problem is that it becomes quite hard for these Abstract Factories to manage Object Lifetime. This is the same problem as illustrated in figure 5.5.
To combat this inflexibility, the only feasible solution is to use a general-purpose DI Container. Because DI Containers analyze constructor signatures using reflection, the Abstract Composition Root doesn’t need to know the Dependencies used to construct its components. The only thing the Abstract Composition Root needs to do is specify the mapping between the Abstraction and the implementation. In other words, the SQL data access Composition Root needs to specify that in case the application requires an IProductRepository, an instance of SqlProductRepository should be created.
Abstract Composition Roots are only required when you truly need to be able to plug in a new assembly without having to recompile any part of the existing application. Most applications don’t need this amount of flexibility. Although you might want to be able to replace the SQL data access layer with an Azure data access layer without having to recompile the domain layer, it’s typically OK if this means you still have to make changes to the startup project.
因为 DI 是一组模式和技术,所以没有任何一种工具可以机械地验证您是否正确应用了它。在第 4 章中,我们研究了描述如何正确使用 DI 的模式,但这只是问题的一方面。研究失败的可能性也很重要,即使是出于好意。你可以从失败中吸取重要的教训,但你不必总是从自己的错误中吸取教训——有时你可以从别人的错误中吸取教训。
Because DI is a set of patterns and techniques, no single tool can mechanically verify whether you’ve applied it correctly. In chapter 4, we looked at patterns that describe how DI can be used properly, but that’s only one side of the coin. It’s also important to study how it’s possible to fail, even with the best of intentions. You can learn important lessons from failure, but you don’t have to always learn from your own mistakes — sometimes you can learn from other people’s mistakes.
In this chapter, we’ve described the most common DI mistakes in the form of anti-patterns. We’ve seen all these mistakes in real life on more than one occasion, and we confess to being guilty of all of them. By now, you should know what to avoid and what you should ideally be doing instead. There can still be issues that look as though they’re hard to solve, however. The next chapter discusses such challenges and how to resolve them.
概括
Summary
反模式是对产生明显负面后果的问题的常见解决方案的描述。
An anti-pattern is a description of a commonly occurring solution to a problem that generates decidedly negative consequences.
Control Freak是本章介绍的最主要的反模式。它有效地阻止您应用任何类型的适当 DI。每当您在Composition Root以外的任何地方依赖 Volatile Dependency时,它都会发生。
Control Freak is the most dominating of the anti-patterns presented in this chapter. It effectively prevents you from applying any kind of proper DI. It occurs every time you depend on a Volatile Dependency in any place other than a Composition Root.
Although the new keyword is a code smell when it comes to Volatile Dependencies, you don’t need to worry about using it for Stable Dependencies. In general, the new keyword isn’t suddenly illegal, but you should refrain from using it to get instances of Volatile Dependencies.
Control Freak违反了依赖倒置原则。
Control Freak is a violation of the Dependency Inversion Principle.
Control Freak代表了大多数编程语言中创建实例的默认方式,因此即使在开发人员从未考虑过 DI 的应用程序中也可以观察到它。这是一种创建新对象的自然而根深蒂固的方式,许多开发人员发现很难放弃。
Control Freak represents the default way of creating instances in most programming languages, so it can be observed even in applications where developers have never considered DI. It’s such a natural and deeply rooted way to create new objects that many developers find it difficult to discard.
A Foreign Default is the opposite of a Local Default. It’s an implementation of a Dependency that’s used as a default even though it’s defined in a different module than its consumer. Dragging along unwanted modules robs you of many of the benefits of loose coupling.
Service Locator is the most dangerous anti-pattern presented in this chapter because it looks like it’s solving a problem. It supplies application components outside the Composition Root with access to an unbounded set of Volatile Dependencies.
Service Locator impacts the reusability of the components consuming it. It makes it non-obvious to a component’s consumers what its Dependencies are, makes such a component dishonest about its level of complexity, and causes its consuming components to drag along the Service Locator as a redundant Dependency.
Service Locator prevents verification of the configuration of relationships between classes. Constructor Injection in combination with Pure DI allows verification at compile time; Constructor Injection in combination with a DI Container allows verification at application startup or as part of a simple automated test.
静态服务定位器会导致相互依赖的测试,因为在执行下一个测试用例时它会保留在内存中。
A static Service Locator causes Interdependent Tests, because it remains in memory when the next test case is executed.
将其确定为服务定位器的不是 API 的机械结构,而是 API 在应用程序中扮演的角色。因此,封装在组合根中的DI 容器不是服务定位器——它是基础结构组件。
It’s not the mechanical structure of an API that determines it as a Service Locator, but rather the role the API plays in the application. Therefore, a DI Container encapsulated in a Composition Root isn’t a Service Locator — it’s an infrastructure component.
Ambient Context supplies application code outside the Composition Root with global access to a Volatile Dependency or its behavior by using static class members.
Ambient Context is similar in structure to the Singleton pattern with the exception that Ambient Context allows its Dependency to be changed. The Singleton pattern ensures that the single created instance will never change.
Ambient Context is usually encountered when developers have a Cross-Cutting Concern as a Dependency that’s used ubiquitously, making them think it justifies moving away from Constructor Injection.
Ambient Context causes the Volatile Dependency to become hidden, complicates testing, and makes it difficult to change the Dependency based on its context.
Constrained Construction forces all implementations of a certain Abstraction to have a particular constructor signature with the goal of enabling late binding. It limits flexibility and might force implementations to do their initialization internally.
可以通过使用通用DI 容器来防止约束构造,因为DI 容器使用反射来分析构造函数签名。
Constrained Construction can be prevented by utilizing a general-purpose DI Container because DI Containers analyze constructor signatures using reflection.
If you can get away with recompiling the startup project, you should keep your Composition Root centralized in the startup project and refrain from using late binding. Late binding introduces extra complexity, and complexity increases maintenance costs.
6
代码味道
6
Code smells
在这一章当中
In this chapter
处理构造函数过度注入的代码异味
Handling Constructor Over-injection code smells
检测和防止过度使用抽象工厂
Detecting and preventing overuse of Abstract Factories
You may have noticed that I (Mark) have a fascination with sauce béarnaise — or sauce hollandaise. One reason is that it tastes so good; another is that it’s a bit tricky to make. In addition to the challenges of production, it presents an entirely different problem: it must be served immediately (or so I thought).
This used to be less than ideal when guests arrived. Instead of being able to casually greet my guests and make them feel welcome and relaxed, I was frantically whipping the sauce in the kitchen, leaving them to entertain themselves. After a couple of repeat performances, my sociable wife decided to take matters into her own hands. We live across the street from a restaurant, so one day she chatted with the cooks to find out whether there’s a trick that would enable me to prepare a genuine hollandaise well in advance. It turns out there is. Now I can serve a delicious sauce for my guests without first subjecting them to an atmosphere of stress and frenzy.
每种工艺都有自己的交易技巧。一般来说,对于软件开发,尤其是 DI,也是如此。挑战不断涌现。在许多情况下,有众所周知的方法来处理它们。多年来,我们看到人们在学习 DI 时遇到困难,并且许多问题在本质上都是相似的。在本章中,我们将了解将 DI 应用于代码库时出现的最常见的代码异味,以及如何解决它们。当我们完成后,您应该能够更好地识别和处理这些情况。
Each craft has its own tricks of the trade. This is also true for software development, in general, and for DI, in particular. Challenges keep popping up. In many cases, there are well-known ways to deal with them. Over the years, we’ve seen people struggle when learning DI, and many of the issues were similar in nature. In this chapter, we’ll look at the most common code smells that appear when you apply DI to a code base and how you can resolve them. When we’re finished, you should be able to better recognize and handle these situations when they occur.
Similar to the two previous chapters in this part of the book, this chapter is organized as a catalog — this time, a catalog of problems and solutions (or, if you will, refactorings). You can read each section independently or in sequence, as you prefer. The purpose of each section is to familiarize you with a solution to a commonly occurring problem so that you’ll be better equipped to deal with it if it occurs. But first, let’s define code smells.
Where an anti-pattern is a description of a commonly occurring solution to a problem that generates decidedly negative consequences, a code smell, on the other hand, is a code construct that might cause problems. Code smells simply warrant further investigation.
6.1 处理构造函数过度注入的代码异味
6.1 Dealing with the Constructor Over-injection code smell
Unless you have special requirements, Constructor Injection (we covered this in chapter 4) should be your preferred injection pattern. Although Constructor Injection is easy to implement and use, it makes developers uncomfortable when their constructors start looking something like that shown next.
public OrderService(
IOrderRepository orderRepository, ① IMessageService messageService, ① IBillingSystem billingSystem, ① ILocationService locationService, ① IInventoryManagement inventoryManagement) ①
{
if (orderRepository == null)
throw new ArgumentNullException("orderRepository");
if (messageService == null)
throw new ArgumentNullException("messageService");
if (billingSystem == null)
throw new ArgumentNullException("billingSystem");
if (locationService == null)
throw new ArgumentNullException("locationService");
if (inventoryManagement == null)
throw new ArgumentNullException("inventoryManagement");
this.orderRepository = orderRepository;
this.messageService = messageService;
this.billingSystem = billingSystem;
this.locationService = locationService;
this.inventoryManagement = inventoryManagement;
}
有很多依赖关系表明单一职责原则(建议零售价) 违反。违反 SRP 会导致代码难以维护。
Having many Dependencies is an indication of a Single Responsibility Principle (SRP) violation. SRP violations lead to code that’s hard to maintain.
In this section, we’ll look at the apparent problem of a growing number of constructor parameters and why Constructor Injection is a good thing rather than a bad thing. As you’ll see, it doesn’t mean you should accept long parameter lists in constructors, so we’ll also review what you can do about those. You can refactor away from Constructor Over-injection in many ways, so we’ll also discuss two common approaches you can take to refactor those occurrences, namely, Facade Services and domain events:
Facade Services 是与参数对象相关的抽象 Facades 1 。2 然而,Facade Service 不是组合组件并将它们作为参数公开,而是仅公开封装的行为,同时隐藏成分。
Facade Services are abstract Facades1 that are related to Parameter Objects.2 Instead of combining components and exposing them as parameters, however, a Facade Service exposes only the encapsulated behavior, while hiding the constituents.
使用域事件,您可以捕获可以触发您正在开发的应用程序状态发生变化的操作。
With domain events, you capture actions that can trigger a change to the state of the application you’re developing.
6.1.1 认识构造函数过度注入
6.1.1 Recognizing Constructor Over-injection
当构造函数的参数列表变得太大时,我们将这种现象称为构造函数过度注入,并将其视为代码异味。3 这是一个与 DI 无关但被 DI 放大的普遍问题。尽管您最初的反应可能是因为构造函数过度注入而放弃构造函数注入,但我们应该庆幸向我们揭示了一个一般的设计问题。
When a constructor’s parameter list grows too large, we call the phenomenon Constructor Over-injection and consider it a code smell.3 It’s a general issue unrelated to, but magnified by, DI. Although your initial reaction might be to dismiss Constructor Injection because of Constructor Over-injection, we should be thankful that a general design issue is revealed to us.
我们不能因为不喜欢清单 6.1中所示的构造函数而责怪任何人,但不要责怪构造函数注入。我们可以同意具有五个参数的构造函数是一种代码味道,但它表明违反了 SRP 而不是与 DI 相关的问题。
We can’t say we blame anyone for disliking a constructor as shown in listing 6.1, but don’t blame Constructor Injection. We can agree that a constructor with five parameters is a code smell, but it indicates a violation of the SRP rather than a problem related to DI.
Our personal threshold lies at four constructor arguments. When we add a third argument, we already begin considering whether we could design things differently, but we can live with four arguments for a few classes. Your limit may be different, but when you cross it, it’s time to investigate.
如何重构已经变得太大的特定类取决于特定情况:已经存在的对象模型、领域、业务逻辑等等。分裂一个崭露头角的神级根据众所周知的设计模式进入更小、更集中的类始终是一个很好的举措。4 不过,在某些情况下,业务需求迫使您同时做许多不同的事情。在应用程序的边界处通常会出现这种情况。考虑触发许多业务事件的粗粒度 Web 服务操作。
How you refactor a particular class that has grown too big depends on the particular circumstances: the object model already in place, the domain, business logic, and so on. Splitting up a budding God Class into smaller, more focused classes according to well-known design patterns is always a good move.4 Still, there are cases where business requirements oblige you to do many different things at the same time. This is often the case at the boundary of an application. Think about a coarse-grained web service operation that triggers many business events.
您可以设计和实施合作者,使他们不违反 SRP。在第 9 章中,我们将讨论 Decorator 5 设计模式如何帮助您堆叠横切关注点,而不是将它们作为服务注入到消费者中。这可以消除许多构造函数参数。在某些场景下,单个入口点需要编排许多Dependencies。一个示例是触发许多不同服务的复杂交互的 Web 服务操作。计划批处理作业的入口点可能面临同样的问题。
You can design and implement collaborators so that they don’t violate the SRP. In chapter 9, we’ll discuss how the Decorator5 design pattern can help you stack Cross-Cutting Concerns instead of injecting them into consumers as services. This can eliminate many constructor arguments. In some scenarios, a single entry point needs to orchestrate many Dependencies. One example is a web service operation that triggers a complex interaction of many different services. The entry point of a scheduled batch job can face the same issue.
The sample e-commerce application that we look at from time to time needs to be able to receive orders. This is often best done by a separate application or subsystem because, at that point, the semantics of the transaction change. As long as you’re looking at a shopping basket, you can dynamically calculate unit prices, exchange rates, and discounts. But when a customer places an order, all of those values must be captured and frozen as they were presented when the customer approved the order. Table 6.1 provides an overview of the order process.
Let’s review how this would look if the consuming OrderService class directly imported all of these Dependencies. The following listing gives a quick overview of the internals of this class.
To keep the example manageable, we omitted most of the details of the class. But it’s not hard to imagine such a class to be rather large and complex. If you let OrderService directly consume all five Dependencies, you get many fine-grained Dependencies. The structure is shown in figure 6.1.
If you use Constructor Injection for the OrderService class (which you should), you have a constructor with five parameters. This is too many and indicates that OrderService has too many responsibilities. On the other hand, all of these Dependencies are required because the OrderService class must implement all of the desired functionality when it receives a new order. You can address this issue by redesigning OrderService using Facade Services refactoring. We’ll show you how to do that in the next section.
6.1.2 从构造函数过度注入到门面服务的重构
6.1.2 Refactoring from Constructor Over-injection to Facade Services
When redesigning OrderService, the first thing you need to do is to look for natural clusters of interaction. The interaction between ILocationService and IInventoryManagement should immediately draw your attention, because you use them to find the closest warehouses that can fulfill the order. This could potentially be a complex algorithm.
After you’ve selected the warehouses, you need to notify them about the order. If you think about this a little further, ILocationService is an implementation detail of notifying the appropriate warehouses about the order. The entire interaction can be hidden behind an IOrderFulfillment interface, like this:
public interface IOrderFulfillment
{
void Fulfill(Order order);
}
下一个清单显示了新IOrderFulfillment接口的实现.
The next listing shows the implementation of the new IOrderFulfillment interface.
public class OrderFulfillment : IOrderFulfillment
{
private readonly ILocationService locationService;
private readonly IInventoryManagement inventoryManagement;
public OrderFulfillment(
ILocationService locationService,
IInventoryManagement inventoryManagement)
{
this.locationService = locationService;
this.inventoryManagement = inventoryManagement;
}
public void Fulfill(Order order)
{
this.locationService.FindWarehouses(...);
this.inventoryManagement.NotifyWarehouses(...);
}
}
有趣的是,订单履行本身听起来很像一个领域概念。您可能发现了一个隐含的域概念并将其显式化。
Interestingly, order fulfillment sounds a lot like a domain concept in its own right. Chances are that you discovered an implicit domain concept and made it explicit.
The default implementation of IOrderFulfillment consumes the two original Dependencies, so it has a constructor with two parameters, which is fine. As a further benefit, you’ve encapsulated the algorithm for finding the best warehouse for a given order into a reusable component. The new IOrderFulfillmentAbstraction is a Facade Service because it hides the two interacting Dependencies with their behavior.
This refactoring merges two Dependencies into one but leaves you with four Dependencies on the OrderService class, as shown in figure 6.2. You also need to look for other opportunities to aggregate Dependencies into a Facade.
The OrderService class only has four Dependencies, and the OrderFulfillment class contains two. That’s not a bad start, but you can simplify OrderService even more. The next thing you may notice is that all the requirements involve notifying other systems about the order. This suggests that you can define a common Abstraction that models notifications, perhaps something like this:
public interface INotificationService
{
void OrderApproved(Order order);
}
Each notification to an external system can be implemented using this interface. But you may wonder how this helps, because you’ve wrapped each Dependency in a new interface. The number of Dependencies didn’t decrease, so did you gain anything?
Yes, you did. Because all three notifications implement the same interface, you can wrap them in a Composite6 pattern as can be seen in listing 6.4. This shows another implementation of INotificationService that wraps a collection of INotificationService instances and invokes the OrderAccepted method on all of those.
public class CompositeNotificationService
: INotificationService ①
{
IEnumerable<INotificationService> services;
public CompositeNotificationService(
IEnumerable<INotificationService> services) ②
{
this.services = services;
}
public void OrderApproved(Order order)
{
foreach (var service in this.services)
{
service.OrderApproved(order); ③
}
}
}
CompositeNotificationService实现并将传入调用转发到其包装的实现。这可以防止消费者不得不处理多个实现,这是一个实现细节。这意味着您可以让depend on a single ,它只留下两个Dependencies,如下所示。INotificationServiceOrderServiceINotificationService
CompositeNotificationService implements INotificationService and forwards an incoming call to its wrapped implementations. This prevents the consumer from having to deal with multiple implementations, which is an implementation detail. This means that you can let OrderService depend on a single INotificationService, which leaves just two Dependencies, as shown next.
From a conceptual perspective, this also makes sense. At a high level, you don’t need to care about the details of how OrderService notifies other systems, but you do care that it does. This reduces OrderService to only two Dependencies, which is a more reasonable number.
From the consumer’s perspective, OrderService is functionally unchanged, making this a true refactoring. On the other hand, on the conceptual level, OrderService is changed. Its responsibility is now to receive an order, save it, and notify other systems. The details of which systems are notified and how this is implemented have been pushed down to a more detailed level. Figure 6.3 shows the final Dependencies of OrderService.
Listing 6.6Composition Root refactored using Facade Services
var repository = new SqlOrderRepository(connectionString);
var notificationService = new CompositeNotificationService(
new INotificationService[]
{
new OrderApprovedReceiptSender(messageService),
new AccountingNotifier(billingSystem),
new OrderFulfillment(locationService, inventoryManagement)
});
var orderServive = new OrderService(repository, notificationService);
Even though you consistently use Constructor Injection throughout, no single class’s constructor ends up requiring more than two parameters. CompositeNotificationService takes an IEnumerable<INotificationService> as a single argument.
一个有益的副作用是,发现这些自然集群会将以前未发现的关系和领域概念公开。在此过程中,您将隐式概念转变为显式概念。7 每个聚合都变成了在更高级别捕获此交互的服务,而消费者的唯一职责就是协调这些更高级别的服务。如果您有一个复杂的应用程序,其中消费者最终对 Facade Services有太多的依赖,您可以重复这个重构。创建 Facade Services 的 Facade Service 是一件非常明智的事情。
A beneficial side effect is that discovering these natural clusters draws previously undiscovered relationships and domain concepts out into the open. In the process, you turn implicit concepts into explicit concepts.7 Each aggregate becomes a service that captures this interaction at a higher level, and the consumer’s single responsibility becomes to orchestrate these higher-level services. You can repeat this refactoring if you have a complex application where the consumer ends up with too many Dependencies on Facade Services. Creating a Facade Service of Facade Services is a perfectly sensible thing to do.
The Facade Services refactoring is a great way to handle complexity in a system. But with regard to the OrderService example, we might even take this one step further, bringing us to domain events.
6.1.3 从构造函数过度注入到领域事件的重构
6.1.3 Refactoring from Constructor Over-injection to domain events
We can say that the act of an order being approved is of importance to the business. These kinds of events are called domain events, and it might be valuable to model them more explicitly in your applications.
Although the introduction of INotificationService is a great improvement to OrderService, it only solves the problem at the level of OrderService and its direct Dependencies. When applying the same refactoring technique to other classes in the system, one could easily imagine how INotificationService evolves toward something similar to the following listing.
Listing 6.7INotificationService with a growing number of methods
public interface INotificationService
{
void OrderApproved(Order order); ① void OrderCancelled(Order order); ① void OrderShipped(Order order); ① void OrderDelivered(Order order); ① void CustomerCreated(Customer customer); ① void CustomerMadePreferred(Customer customer); ①
}
Within any system of reasonable size and complexity, you’d easily get dozens of these domain events, which would lead to an ever-changing INotificationService interface. With each change to this interface, all implementations of that interface must be updated too. Additionally, ever-growing interfaces also causes ever-growing implementations. If, however, you promote the domain events to actual types and make them part of the domain, as shown in figure 6.4, an interesting opportunity to generalize even further arises.
Listing 6.8OrderApproved and OrderCancelled domain event classes
public class OrderApproved
{
public readonly Guid OrderId;
public OrderApproved(Guid orderId)
{
this.OrderId = orderId;
}
}
public class OrderCancelled
{
public readonly Guid OrderId;
public OrderCancelled(Guid orderId)
{
this.OrderId = orderId;
}
}
Although both the OrderApproved and OrderCancelled classes have the same structure and are related to the same Entity, modelling them around their own class makes it easier to create code that responds to such a specific event. When each domain event in your system gets its own type, it lets you change INotificationService to a generic interface with a single method, as the following listing shows.
In the case of IEventHandler<TEvent>, a class deriving from the interface must specify a TEvent type — for the instance OrderCancelled — in the class declaration. This type will then be used as the parameter type for that class’s Handle method. This allows one interface to unify several classes, despite differences in their types. In addition, it allows each of those implementations to be strongly typed, working exclusively off whatever type was specified as TEvent.
Based on this interface, you can now build the classes that respond to a domain event, like the OrderFulfillment class you saw previously. Based on the new IEventHandler<TEvent> interface, the original OrderFulfillment class, as shown in listing 6.3, changes to that displayed in the following listing.
The OrderFulfillment class implements IEventHandler<OrderApproved>, meaning that it acts on OrderApproved events. OrderService then uses the new IEventHandler<TEvent> interface, as figure 6.5 shows.
Listing 6.11 shows an OrderService depending on IEventHandler<OrderApproved>. Compared to listing 6.5, the OrderService logic will stay almost unchanged.
Just as with the non-generic INotificationService, you still need a Composite that takes care of dispatching the information to the list of available handlers. This enables you to add new handlers to the application, without the need to change OrderService. Listing 6.12 shows this Composite. As you can see, it’s similar to the CompositeNotificationService from listing 6.4.
public class CompositeEventHandler<TEvent> : IEventHandler<TEvent>
{
private readonly IEnumerable<IEventHandler<TEvent>> handlers;
public CompositeEventHandler(
IEnumerable<IEventHandler<TEvent>> handlers) ①
{
this.handlers = handlers;
}
public void Handle(TEvent e)
{
foreach (var handler in this.handlers)
{
handler.Handle(e);
}
}
}
IEventHandler<TEvent>像 那样包装实例集合CompositeEventHandler<TEvent>,让您可以向系统添加任意事件处理程序实现,而无需对IEventHandler<TEvent>. 使用 new CompositeEventHandler<TEvent>,您可以创建OrderService及其依赖项。
Wrapping a collection of IEventHandler<TEvent> instances, as does CompositeEventHandler<TEvent>, lets you add arbitrary event handler implementations to the system without having to make any changes to consumers of IEventHandler<TEvent>. Using the new CompositeEventHandler<TEvent>, you can create the OrderService with its Dependencies.
Listing 6.13Composition Root for the OrderService refactored using events
var orderRepository = new SqlOrderRepository(connectionString);
var orderApprovedHandler = new CompositeEventHandler<OrderApproved>(
new IEventHandler<OrderApproved>[]
{
new OrderApprovedReceiptSender(messageService),
new AccountingNotifier(billingSystem),
new OrderFulfillment(locationService, inventoryManagement)
});
var orderService = new OrderService(orderRepository, orderApprovedHandler);
Likewise, the Composition Root will contain the configuration for the handlers of other domain events. The following code shows a few more event handlers for OrderCancelled and CustomerCreated. We leave it up to the reader to extrapolate from this.
var orderCancelledHandler = new CompositeEventHandler<OrderCancelled>(
new IEventHandler<OrderCancelled>[]
{
new AccountingNotifier(billingSystem),
new RefundSender(orderRepository),
});
var customerCreatedHandler = new CompositeEventHandler<CustomerCreated>(
new IEventHandler<CustomerCreated>[]
{
new CrmNotifier(crmSystem),
new TermsAndConditionsSender(messageService, termsRepository),
});
var orderService = new OrderService(
orderRepository, orderApprovedHandler, orderCancelledHandler);
var customerService = new CustomerService(
customerRepository, customerCreatedHandler);
The beauty of a generic interface like IEventHandler<TEvent> is that the addition of new features won’t cause any changes to either the interface nor any of the already existing implementations. In case you need to generate an invoice for your approved order, you only have to add a new implementation that implements IEventHandler<OrderApproved>. When a new domain event is created, no changes to CompositeEventHandler<TEvent> are required.
In a sense, IEventHandler<TEvent> becomes a template for common building blocks that the application relies on. Each building block responds to a particular event. As you saw, you can have multiple building blocks that respond to the same event. New building blocks can be plugged in without the need to change any existing business logic.
Although the introduction of IEventHandler<TEvent> prevented the problem of an ever-growing INotificationService, it doesn’t prevent the problem of an ever-growing OrderService class. This is something we’ll address in great detail in chapter 10.
We’ve found the use of domain events to be an effective model. It allows code to be defined on a more conceptual level, while letting you build more-robust software, especially where you have to communicate with external systems that aren’t part of your database transaction. But no matter which refactoring approach you choose, be it Decorators, Facade Services, domain events, or perhaps another, the important takeaway here is that Constructor Over-injection is a clear sign that code smells. Don’t ignore such a sign, but act accordingly.
Because Constructor Over-injection is a commonly recurring code smell, the next section discusses a more subtle problem that, at first sight, might look like a good solution to a set of recurring problems. But is it?
6.2 滥用抽象工厂
6.2 Abuse of Abstract Factories
当您开始应用 DI 时,您可能遇到的第一个困难是抽象依赖于运行时值。例如,在线地图站点可能会提供计算两个位置之间的路线的功能,让您可以选择路线的计算方式。你想要最短路线吗?基于已知交通模式的最快路线?风景最美的路线?
When you start applying DI, one of the first difficulties you’re likely to encounter is when Abstractions depend on runtime values. For example, an online mapping site may offer to calculate a route between two locations, giving you a choice of how you want the route computed. Do you want the shortest route? The fastest route based on known traffic patterns? The most scenic route?
在这种情况下,许多开发人员的第一反应是使用抽象工厂。尽管抽象工厂在软件中确实占有一席之地,但当涉及到 DI 时——当工厂被用作应用程序组件中的依赖项时——它们经常被过度使用。在许多情况下,存在更好的选择。
The first response from many developers in such cases would be to use an Abstract Factory. Although Abstract Factories do have their place in software, when it comes to DI — when factories are used as DEPENDENCIES in application components — they're often overused. In many cases, better alternatives exist.
In this section, we’ll discuss two cases where better alternatives to Abstract Factories exist. In the first case, we’ll discuss why Abstract Factories shouldn’t be used to create stateful Dependencies with a short lifetime. After that, we’ll discuss why it’s generally better not to use Abstract Factories to select Dependencies based on runtime data.
6.2.1 滥用抽象工厂解决生命周期问题
6.2.1 Abusing Abstract Factories to overcome lifetime problems
When it comes to the abuse of Abstract Factories, a common code smell is to see parameterless factory methods that have a Dependency as the return type, as the next listing shows.
Abstract Factories with parameterless Create methods are often used to allow consumers to control the lifetime of their Dependencies. In the following listing, HomeController controls the lifetime of IProductRepository by requesting it from the factory, and disposing of it when it finishes using it.
Listing 6.15 A HomeController explicitly managing its Dependency’s lifetime
public class HomeController : Controller
{
private readonly IProductRepositoryFactory factory;
public HomeController(
IProductRepositoryFactory factory) ①
{
this.factory = factory;
}
public ViewResult Index()
{
using (IProductRepository repository =
this.factory.Create()) ②
{
var products =
repository.GetFeaturedProducts(); ③
return this.View(products);
} ④
}
}
Figure 6.6 The consuming class HomeController controls the lifetime of its IProductRepositoryDependency. It does so by requesting a Repository instance from the IProductRepositoryFactoryDependency and calling Dispose on the IProductRepository instance when it’s done with it.
Disposing the Repository is required when the used implementation holds on to resources, such as database connections, that should be closed in a deterministic fashion. Although an implementation might require deterministic cleanup, that doesn’t imply that it should be the responsibility of the consumer to ensure proper cleanup. This brings us to the concept of Leaky Abstractions.
Just as Test-Driven Development (TDD) ensures Testability, it’s safest to define interfaces first and then subsequently program against them. Even so, there are cases where you already have a concrete type and now want to extract an interface. When you do this, you must take care that the underlying implementation doesn’t leak through. One way this can happen is if you only extract an interface from a given concrete type, but some of the parameter or return types are still concrete types defined in the library you want to abstract from. The following interface definition offers an example:
public interface IRequestContext ①
{
HttpContext Context { get; } ②
}
If you need to extract an interface, you need to do it in a recursive manner, ensuring that all types exposed by the root interface are themselves interfaces. We call this Deep Extraction, and the result is Deep Interfaces.
This doesn’t mean that interfaces can’t expose any concrete classes. It’s typically fine to expose behaviorless data objects, such as Parameter Objects, view models, and Data Transfer Objects (DTOs). They’re defined in the same library as the interface instead of the library you want to abstract from. Those data objects are part of the Abstraction.
Be careful with Deep Extraction: it doesn’t always lead to the best solution. Take the previous example. Consider the following suspicious-looking implementation of a Deep Extracted IHttpContext interface:
public interface IHttpContext ①
{
IHttpRequest Request { get; } ② IHttpResponse Response { get; } ② IHttpSession Session { get; } ② IPrincipal User { get; } ②
}
Although you might be using interfaces all the way down, it’s still glaringly obvious that the HTTP model is leaking through. In other words, IHttpContext is still a Leaky Abstraction — and so are its sub-interfaces.
你应该如何建模IRequestContext呢?要弄清楚这一点,您必须了解其消费者想要实现的目标。例如,如果消费者需要找出发送当前 Web 请求的用户的角色,您最终可能会使用我们在第 3 章中讨论的方法:IUserContext
How should you model IRequestContext instead? To figure this out, you have to look at what its consumers want to achieve. For instance, if a consumer needs to find out the role of the user who sent the current web request, you might end up instead with the IUserContext we discussed in chapter 3:
public interface IUserContext
{
bool IsInRole(Role role);
}
这个IUserContext界面不会向消费者透露它作为 ASP.NET Web 应用程序的一部分运行。事实上,此抽象允许您将相同的使用者作为 Windows 服务或桌面应用程序的一部分运行。它可能需要创建一个不同的IUserContext实现,但它的消费者并没有注意到这一点。
This IUserContext interface doesn’t reveal to the consumer that it’s running as part of an ASP.NET web application. As a matter of fact, this Abstraction lets you run the same consumer as part of a Windows service or desktop application. It’ll likely require the creation of a different IUserContext implementation, but its consumers are oblivious to this.
Always consider whether a given Abstraction makes sense for implementations other than the one you have in mind. If it doesn’t, you should reconsider your design. That brings us back to our parameterless factory methods.
无参数工厂方法是Leaky Abstractions
Parameterless factory methods are Leaky Abstractions
As useful as the Abstract Factory pattern can be, you must take care to apply it with discrimination. The Dependencies created by an Abstract Factory should conceptually require a runtime value, and the translation from a runtime value into an Abstraction should make sense. If you feel the urge to introduce an Abstract Factory because you have a specific implementation in mind, you may have a Leaky Abstraction at hand.
Consumers that depend on IProductRepository, such as the HomeController from listing 6.15, shouldn’t care about which instance they get. At runtime, you might need to create multiple instances, but as far as the consumer is concerned, there’s only one.
By specifying an IProductRepositoryFactoryAbstraction with a parameterless Create method, you let the consumer know that there are more instances of the given service, and that it has to deal with this. Because another implementation of IProductRepository might not require multiple instances or deterministic disposal at all, you’re therefore leaking implementation details through the Abstract Factory with its parameterless Create method. In other words, you’ve created a Leaky Abstraction.
接下来,我们将讨论如何防止这种Leaky Abstraction代码异味。
Next, we’ll discuss how to prevent this Leaky Abstraction code smell.
Consuming code shouldn’t be concerned with the possibility of there being more than one IProductRepository instance. You should therefore get rid of the IProductRepositoryFactory completely and instead let consumers depend solely on IProductRepository, which they should have injected using Constructor Injection. This advice is reflected in the following listing.
Listing 6.16HomeController without managing its Dependency’s lifetime
public class HomeController : Controller
{
private readonly IProductRepository repository;
public HomeController(
IProductRepository repository) ①
{
this.repository = repository;
}
public ViewResult Index()
{
var products = ② this.repository.GetFeaturedProducts(); ② ② return this.View(products); ②
}
}
Figure 6.7 Compared to figure 6.6, removing the responsibility of managing IProductRepository’s lifetime together with removing the IProductRepositoryFactoryDependency considerably simplifies interaction with HomeController’s Dependencies.
Although removing Lifetime Management simplifies the HomeController, you’ll have to manage the Repository’s lifetime somewhere in the application. A common pattern to address this problem is the Proxy pattern, an example of which is given in the next listing.
Listing 6.17 Delaying creation of SqlProductRepository using a Proxy
public class SqlProductRepositoryProxy : IProductRepository
{
private readonly string connectionString;
public SqlProductRepositoryProxy(string connectionString)
{
this.connectionString = connectionString;
}
public IEnumerable<Product> GetFeaturedProducts()
{
using (var repository = this.Create()) ①
{
return repository.GetFeaturedProducts(); ②
}
}
private SqlProductRepository Create()
{
return new SqlProductRepository( ③
this.connectionString);
}
}
Notice how SqlProductRepositoryProxy internally contains factory-like behavior with its private Create method. This behavior, however, is encapsulated within the Proxy and doesn’t leak out, compared to the IProductRepositoryFactory Abstract Factory that exposes IProductRepository from its definition.
SqlProductRepositoryProxy is tightly coupled to SqlProductRepository. This would be an implementation of the Control Freak anti-pattern (section 5.1) if the SqlProductRepositoryProxy was defined in your domain layer. Instead, you should either define this Proxy in your data access layer that contains SqlProductRepository or, more likely, the Composition Root.
Because the Create method composes part of the object graph, the Composition Root is a well-suited location to place this Proxy class. The next listing shows the structure of the Composition Root using the SqlProductRepositoryProxy.
In the case that an Abstraction has many members, it becomes quite cumbersome to create Proxy implementations. Abstractions with many members, however, typically violate the Interface Segregation Principle. Making Abstractions more focused solves many problems, such as the complexity of creating Proxies, Decorators, and Test Doubles. We’ll discuss this in more detail in section 6.3 and again come back to this subject in chapter 10.
下一节将讨论滥用抽象工厂来根据提供的运行时数据选择要返回的依赖项。
The next section deals with the abuse of Abstract Factories to select the Dependency to return, based on the supplied runtime data.
6.2.2 滥用抽象工厂根据运行时数据选择依赖
6.2.2 Abusing Abstract Factories to select Dependencies based on runtime data
In the previous section, you learned that Abstract Factories should typically accept runtime values as input. Without them, you’re leaking implementation details about the implementation to the consumer. This doesn’t mean that an Abstract Factory that accepts runtime data is the correct solution to every situation. More often than not, it isn’t.
In this section, we’ll look at Abstract Factories that accept runtime data specifically to decide which Dependency to return. The example we’ll look at is the online mapping site that offers to calculate a route between two locations, which we introduced at the start of section 6.2.
To calculate a route, the application needs a routing algorithm, but it doesn’t care which one. Each option represents a different algorithm, and the application can handle each routing algorithm as an Abstraction to treat them all equally. You must tell the application which algorithm to use, but you won’t know this until runtime because it’s based on the user’s choice.
在 Web 应用程序中,您只能将基本类型从浏览器传输到服务器。当用户从下拉框中选择路由算法时,您必须用数字或字符串来表示。15 An是一个数字,所以在服务器上你可以使用这个来表示选择:enumRouteType
In a web application, you can only transfer primitive types from the browser to the server. When the user selects a routing algorithm from a drop-down box, you must represent this by a number or a string.15 An enum is a number, so on the server you can represent the selection using this RouteType:
public enum RouteType { Shortest, Fastest, Scenic }
您需要的是一个可以为您计算路线的实例:IRouteAlgorithm
What you need is an instance of IRouteAlgorithm that can calculate the route for you:
public interface IRouteAlgorithm
{
RouteResult CalculateRoute(RouteSpecification specification);
}
现在你遇到了一个问题。这是基于用户选择的运行时数据。它与请求一起发送到服务器。RouteType
Now you’re presented with a problem. The RouteType is runtime data based on the user’s choice. It’s sent to the server with the request.
Listing 6.19RouteController with its GetRoute method
public class RouteController : Controller
{
public ViewResult GetRoute(
RouteSpecification spec, RouteType routeType)
{
IRouteAlgorithm algorithm = ... ① var route = algorithm.CalculateRoute(spec); ② var vm = new RouteViewModel ③ { ③ ... ③ }; ③ return this.View(vm); ④
}
}
The question now becomes, how do you get the appropriate algorithm? If you hadn’t been reading this chapter, your knee-jerk reaction to this challenge would probably be to introduce an Abstract Factory, like this:
public interface IRouteAlgorithmFactory
{
IRouteAlgorithm CreateAlgorithm(RouteType routeType);
}
This enables you to implement a GetRoute method for RouteController by injecting IRouteAlgorithmFactory and using it to translate the runtime value to the IRouteAlgorithmDependency you need. The following listing demonstrates the interaction.
Listing 6.20 Using an IRouteAlgorithmFactory in RouteController
public class RouteController : Controller
{
private readonly IRouteAlgorithmFactory factory;public RouteController(IRouteAlgorithmFactory factory){this.factory = factory;}
public ViewResult GetRoute(
RouteSpecification spec, RouteType routeType)
{
IRouteAlgorithm algorithm = ① this.factory.CreateAlgorithm(routeType); ① var route = algorithm.CalculateRoute(spec); ②
var vm = new RouteViewModel
{
...
};
return this.View(vm);
}
}
The RouteController class’s responsibility is to handle web requests. The GetRoute method receives the user’s specification of origin and destination, as well as a selected RouteType. With an Abstract Factory, you map the runtime RouteType value to an IRouteAlgorithm instance, so you request an instance of IRouteAlgorithmFactory using Constructor Injection. This sequence of interactions between RouteController and its Dependencies is shown in figure 6.8.
The most simple implementation of IRouteAlgorithmFactory would involve a switch statement and return three different implementations of IRouteAlgorithm based on the input. But we’ll leave this as an exercise for the reader.
Up until this point you might be wondering, “What’s the catch? Why is this a code smell?” To be able to see the problem, we need to go back to the Dependency Inversion Principle.
Figure 6.8RouteController supplies the routeType runtime value to IRouteAlgorithmFactory. The factory returns an IRouteAlgorithm implementation, and RouteController requests a route by calling CalculateRoute. The interaction is similar to that of figure 6.6.
In chapter 3 (section 3.1.2), we talked about the Dependency Inversion Principle. We discussed how it states that Abstractions should be owned by the layer using the Abstraction. We explained that it’s the consumer of the Abstraction that should dictate its shape and define the Abstraction in a way that suits its needs the most. When we go back to our RouteController and ask ourselves whether this is the design that suits RouteController the best, we’d argue that this design doesn’t suit RouteController.
One way of looking at this is by evaluating the number of DependenciesRouteController has, which tells you something about the complexity of the class. As you saw in section 6.1, having a large number of Dependencies is a code smell, and a typical solution is to apply Facade Services refactoring.
When you introduce an Abstract Factory, you always increase the number of Dependencies a consumer has. If you only look at the constructor of RouteController, you may be led to believe that the controller only has one Dependency. But IRouteAlgorithm is also a Dependency of RouteController, even if it isn’t injected into its constructor.
这种增加的复杂性一开始可能并不明显,但是当您开始单元测试时可以立即感受到RouteController。这不仅迫使您测试与 with 的交互RouteController,IRouteAlgorithm您还必须测试与 的交互IRouteAlgorithmFactory。
This increased complexity might not be obvious at first, but it can be felt instantly when you start unit testing RouteController. Not only does this force you to test the interaction RouteController has with IRouteAlgorithm, you also have to test the interaction with IRouteAlgorithmFactory.
You can reduce the number of Dependencies by merging both IRouteAlgorithmFactory and IRouteAlgorithm together, much like you saw with the Facade Services refactoring of section 6.1. Ideally, you’d want to use the Proxy pattern the same way you applied it in section 6.2.1. A Proxy, however, is only applicable in case the Abstraction is supplied with all the data required to select the appropriate Dependency. Unfortunately, this prerequisite doesn’t hold for IRouteAlgorithm because it’s only supplied with a RouteSpecification, but not a RouteType.
Before you discard the Proxy pattern, it’s important to verify whether it makes sense from a conceptual level to pass RouteType on to IRouteAlgorithm. If it does, it means that a CalculateRoute implementation contains all the information required to select both the proper algorithm and the runtime values the algorithm will need to calculate the route. In this case, however, passing RouteType on to IRouteAlgorithm is conceptually weird. An algorithm implementation will never need to use RouteType. Instead, to reduce the controller’s complexity, you define an Adapter that internally dispatches to the appropriate route algorithm:
public interface IRouteCalculator
{
RouteResult Calculate(RouteSpecification spec, RouteType routeType);
}
Listing 6.21 Using an IRouteCalculator in RouteController
public class RouteController : Controller
{
private readonly IRouteCalculator calculator;public RouteController(IRouteCalculator calculator) ① {this.calculator = calculator;}
public ViewResult GetRoute(RouteSpecification spec, RouteType routeType)
{
var route = this.calculator.Calculate(spec, routeType);
var vm = new RouteViewModel { ... };
return this.View(vm);
}
}
Figure 6.9 shows the simplified interaction between RouteController and its sole Dependency. As you saw in figure 6.7, the interaction is reduced to a single method call.
Figure 6.9 Compared to figure 6.8, by hiding IRouteAlgorithmFactory and IRouteAlgorithm behind a single IRouteCalculatorAbstraction, the interaction between RouteController and its (now single) Dependency is simplified.
You can implement an IRouteCalculator in many ways. One way is to inject IRouteAlgorithmFactory into this RouteCalculator. This isn’t our preference, though, because IRouteAlgorithmFactory would be a useless extra layer of indirection you could easily do without. Instead, you’ll inject IRouteAlgorithm implementations into the RouteCalculator constructor.
Listing 6.22IRouteCalculator wrapping a dictionary of IRouteAlgorithms
public class RouteCalculator : IRouteCalculator
{
private readonly IDictionary<RouteType, IRouteAlgorithm> algorithms;
public RouteCalculator(
IDictionary<RouteType, IRouteAlgorithm> algorithms)
{
this.algorithms = algorithms;
}
public RouteResult Calculate(RouteSpecification spec, RouteType type)
{
return this.algorithms[type].CalculateRoute(spec);
}
}
使用新定义的RouteCalculator,RouteController现在可以这样构造:
Using the newly defined RouteCalculator, RouteController can now be constructed like this:
var algorithms = new Dictionary<RouteType, IRouteAlgorithm>
{
{ RouteType.Shortest, new ShortestRouteAlgorithm() },
{ RouteType.Fastest, new FastestRouteAlgorithm() },
{ RouteType.Scenic, new ScenicRouteAlgorithm() }
};
new RouteController(
new RouteCalculator(algorithms));
By refactoring from Abstract Factory to an Adapter, you effectively reduce the number of Dependencies between your components. Figure 6.10 shows the Dependency graph of the initial solution using the Factory, while figure 6.11 shows the object graph after refactoring.
图 6.10 with 的初始依赖关系图RouteControllerIRouteAlgorithmFactory
Figure 6.10 The initial Dependency graph for RouteController with IRouteAlgorithmFactory
When you use Abstract Factories to select Dependencies based on supplied runtime data, more often than not, you can reduce complexity by refactoring toward Adapters that don’t expose the underlying Dependency like the Abstract Factory does. This, however, doesn’t hold only when dealing with Abstract Factories. We’d like to generalize this point.
Typically, service Abstractions shouldn’t expose other service Abstractions in their definition.16 This means that a service Abstraction shouldn’t accept another service Abstraction as input, nor should it have service Abstractions as output parameters or as a return type. Application services that depend on other application services force their clients to know about both Dependencies.
The next code smell is a more exotic one, so you might not encounter it that often. Although the previously discussed code smells can go unnoticed, the next smell is hard to miss — your code either stops compiling or breaks at runtime.
Occasionally, Dependency implementations turn out to be cyclic. An implementation requires another Dependency whose implementation requires the first Abstraction. Such a Dependency graph can’t be satisfied. Figure 6.12 shows this problem.
The following shows a simplistic example containing the cyclic Dependency of figure 6.12:
public class Chicken : IChicken
{
public Chicken(IEgg egg) { ... } ①
public void HatchEgg() { ... }
}
public class Egg : IEgg ②
{
public Egg(IChicken chicken) { ... } ③
}
考虑到前面的示例,您如何构建由这些类组成的对象图?
With the previous example in mind, how can you construct an object graph consisting of these classes?
What we’ve got here is your typical the chicken or the egg causality dilemma. The short answer is that you can’t construct an object graph like this because both classes require the other object to exist before they’re constructed. As long as the cycle remains, you can’t possibly satisfy all Dependencies, and your applications won’t be able to run. Clearly, something must be done, but what?
In this section, we’ll look into the issue concerning cyclic Dependencies, including an example. When we’re finished, your first reaction should be to try to redesign your Dependencies, because the problem is typically caused by your application’s design. The main takeaway from this section, therefore, is this: Dependency cycles are typically caused by an SRP violation.
If redesigning your Dependencies isn’t possible, you can break the cycle by refactoring from Constructor Injection to Property Injection. This represents a loosening of a class’s invariants, so it isn’t something you should do lightly.
6.3.1 示例:SRP 违规导致的依赖循环
6.3.1 Example: Dependency cycle caused by an SRP violation
Mary Rowan(第 2 章的开发人员)开发她的电子商务应用程序已经有一段时间了,并且在生产中非常成功。然而,有一天,玛丽的老板突然上门要求一项新功能。抱怨是当生产中出现问题时,很难确定谁在处理系统中的特定数据。一种解决方案是将更改存储在审计表中,该表记录系统中每个用户所做的每项更改。
Mary Rowan (our developer from chapter 2) has been developing her e-commerce application for some time now, and it’s been quite successful in production. One day, however, Mary’s boss pops around the door to request a new feature. The complaint is that when problems arise in production, it’s hard to pinpoint who’s been working on a certain piece of data in the system. One solution would be to store changes in an auditing table that records every change that every user in the system makes.
After thinking about this for some time, Mary comes up with the definition for an IAuditTrailAppenderAbstraction, as shown in listing 6.23. (Note that to demonstrate this code smell in a realistic setting, we need a somewhat complex example. The following example consists of three classes, and we’ll spend a few pages explaining the code, before we get to its analysis.)
public interface IAuditTrailAppender ①
{
void Append(Entity changedEntity); ②
}
Mary 使用 SQL Server Management Studio 创建一个 AuditEntries 表,她可以用它来存储审计条目。表定义如表 6.2所示。
Mary uses SQL Server Management Studio to create an AuditEntries table that she can use to store the audit entries. The table definition is shown in table 6.2.
Listing 6.24SqlAuditTrailAppender appends entries to a SQL database table
public class SqlAuditTrailAppender : IAuditTrailAppender
{
private readonly IUserContext userContext;
private readonly CommerceContext context;
private readonly ITimeProvider timeProvider; ①
public SqlAuditTrailAppender(
IUserContext userContext,
CommerceContext context,
ITimeProvider timeProvider)
{
this.userContext = userContext;
this.context = context;
this.timeProvider = timeProvider;
}
public void Append(Entity changedEntity)
{
AuditEntry entry = new AuditEntry
{
UserId = this.userContext.CurrentUser.Id, ② TimeOfChange = this.timeProvider.Now, ② EntityId = entity.Id, ② EntityType = entity.GetType().Name ②
};
this.context.AuditEntries.Add(entry);
}
}
审计跟踪的一个重要部分是将更改与用户相关联。为此,SqlAuditTrailAppender需要一个IUserContextDependency。这允许使用 上的属性构造条目。这是 Mary 前段时间为另一个功能添加的属性。SqlAuditTrailAppenderCurrentUserIUserContext
An important part of an audit trail is relating a change to a user. To accomplish this, SqlAuditTrailAppender requires an IUserContextDependency. This allows SqlAuditTrailAppender to construct the entry using the CurrentUser property on IUserContext. This is a property that Mary added some time ago for another feature.
清单 6.25显示了 Mary 的当前版本(初始版本见清单 3.12)。AspNetUserContextAdapter
Listing 6.25 shows Mary’s current version of the AspNetUserContextAdapter (see listing 3.12 for the initial version).
Listing 6.25AspNetUserContextAdapter with added CurrentUser property
public class AspNetUserContextAdapter : IUserContext
{
private static HttpContextAccessor Accessor = new HttpContextAccessor();
private readonly IUserRepository repository; ①
public AspNetUserContextAdapter(
IUserRepository repository)
{
this.repository = repository;
}
public User CurrentUser ②
{
get
{
var user = Accessor.HttpContext.User; ③ string userName = user.Identity.Name; ③ return this.repository.GetByName(userName); ③
}
}
...
}
当您忙于阅读有关 DI 模式和反模式的内容时,Mary 也很忙。IUserRepository是她同时添加的抽象之一。我们将很快讨论她的IUserRepository实现。
While you were busy reading about DI patterns and anti-patterns, Mary’s been busy too. IUserRepository is one of the Abstractions she added in the meantime. We’ll discuss her IUserRepository implementation shortly.
Mary 的下一步是更新需要附加到审计跟踪的类。需要更新的类之一是SqlUserRepository. 它实现IUserRepository了 ,所以现在是看一看它的好时机。以下清单显示了此类的相关部分。
Mary’s next step is to update the classes that need to be appended to the audit trail. One of the classes that needs to be updated is SqlUserRepository. It implements IUserRepository, so this is a good moment to take a peek at it. The following listing shows the relevant parts of this class.
Listing 6.26SqlUserRepository that needs to append to the audit trail
public class SqlUserRepository : IUserRepository
{
public SqlUserRepository(
CommerceContext context,
IAuditTrailAppender appender) ①
{
this.appender = appender;
this.context = context;
}
public void Update(User user)
{
this.appender.Append(user); ② ... ③
}
public User GetById(Guid id) { ... } ③ public User GetByName(string name) { ... } ④
}
Mary is almost finished with her feature. Because she added a constructor argument to the SqlUserRepository method, she’s left with updating the Composition Root. Currently, the part of the Composition Root that creates AspNetUserContextAdapter looks like this:
var userRepository = new SqlUserRepository(context);
IUserContext userContext = new AspNetUserContextAdapter(userRepository);
Because IAuditTrailAppender was added as Dependency to the SqlUserRepository constructor, Mary tries to add it to the Composition Root:
var appender = new SqlAuditTrailAppender(userContext, ① context,timeProvider);
var userRepository = new SqlUserRepository(context, appender);
IUserContext userContext = new AspNetUserContextAdapter(userRepository);
Because SqlAuditTrailAppender depends on IUserContext, Mary tries to supply the SqlAuditTrailAppender with the userContext variable that she defined. The C# compiler doesn’t accept this because such a variable must be defined before it’s used. Mary tries to fix the problem by moving the definition and assignment of the userContext variable up, but this immediately causes the C# compiler to complain about the userRepository variable. But when she moves the userRepository variable up, the compiler complaints about the appender variable, which is used before it’s declared.
玛丽开始意识到她遇到了严重的麻烦——她的依赖关系图中有一个循环。让我们分析一下哪里出了问题。
Mary starts to realize she’s in serious trouble — there’s a cycle in her Dependency graph. Let’s analyze what went wrong.
The cycle in Mary’s object graph appeared once she added the IAuditTrailAppenderDependency to the SqlUserRepository class. Figure 6.13 shows this Dependency cycle.
The figure shows the cycle in the object graph. The object graph, however, is part of the story. Another view we can use to analyze the problem is the method call graph as shown here:
This call graph shows how the call would start with the UpdateMailAddress method of UserService, which would call into the Update method of the SqlUserRepository class. From there it goes into SqlAuditTrailAppender, then into AspNetUserContextAdapter and, finally, it ends up in the SqlUserRepository’s GetByName method.
这个方法调用图表明,虽然对象图是循环的,但方法调用图不是递归的。GetByName例如,如果再次调用,它将变为递归SqlAuditTrailAppender.Append。这将导致无休止地调用其他方法,直到进程用完堆栈空间,从而导致. 对 Mary 来说幸运的是,调用图不是递归的,因为这需要她重写方法。问题的原因在别处——违反了 SRP。StackOverflowException
What this method call graph shows is that although the object graph is cyclic, the method call graph isn’t recursive. It would become recursive if GetByName again called SqlAuditTrailAppender.Append, for instance. That would cause the endless calling of other methods until the process ran out of stack space, causing a StackOverflowException. Fortunately for Mary, the call graph isn’t recursive, as that would require her to rewrite the methods. The cause of the problem lies somewhere else — there’s an SRP violation.
When we take a look at the previously declared classes AspNetUserContextAdapter, SqlUserRepository, and SqlAuditTrailAppender, you might find it difficult to spot a possible SRP violation. All three classes seem to be focused on one particular area, as table 6.3 lists.
If you look more closely at IUserRepository, you can see that the functionality in the class is primarily grouped around the concept of a user. This is a quite broad concept. If you stick with this approach of grouping user-related methods in a single class, you’ll see both IUserRepository and SqlUserRepository being changed quite frequently.
When we look at the SRP from the perspective of cohesion, we can ask ourselves whether the methods in IUserRepository are really that highly cohesive. How easy would it be to split the class up into multiple narrower interfaces and classes?
6.3.3 重构 SRP 违规以解决依赖循环
6.3.3 Refactoring from SRP violations to resolve the Dependency cycle
It might not always be easy to fix SRP violations, because that might cause rippling changes through the consumers of the Abstraction. In the case of our little commerce application, however, it’s quite easy to make the change, as the following listing shows.
Listing 6.27GetByName moved into IUserByNameRetriever
public interface IUserByNameRetriever
{
User GetByName(string name); ①
}
public class SqlUserByNameRetriever : IUserByNameRetriever
{
public SqlUserByNameRetriever(CommerceContext context)
{
this.context = context;
}
public User GetByName(string name) { ... }
}
In the listing, the GetByName method is extracted from IUserRepository and SqlUserRepository into a new Abstraction implementation pair named IUserByNameRetriever and SqlUserByNameRetriever. The new SqlUserByNameRetriever implementation doesn’t depend on IAuditTrailAppender. The remaining part of SqlUserRepository is shown next.
Listing 6.28 The remaining part of IUserRepository and its implementation
public interface IUserRepository ①
{
void Update(User user);
User GetById(Guid id);
}
public class SqlUserRepository : IUserRepository
{
public SqlUserRepository( ②
CommerceContext context,
IAuditTrailAppender appender
{
this.context = context;
this.appender = appender;
}
public void Update(User user) { ... }
public User GetById(Guid id) { ... }
}
Mary gained a couple of things from this division. First of all, the new classes are smaller and easier to comprehend. Next, it lowers the chance of getting into the situation where Mary will be constantly updating existing code. And last, but not least, splitting the SqlUserRepository class breaks the Dependency cycle, because the new SqlUserByNameRetriever doesn’t depend on IAuditTrailAppender. Figure 6.14 shows how the Dependency cycle was broken.
Figure 6.14 The separation of IUserRepository into two interfaces breaks the Dependency cycle.
以下代码显示了将所有内容联系在一起的新Composition Root :
The following code shows the new Composition Root that ties everything together:
var userContext = new AspNetUserContextAdapter(
new SqlUserByNameRetriever(context)); ①
var appender = new
SqlAuditTrailAppender(
userContext,
context,
timeProvider);
var repository = new SqlUserRepository(context, appender);
The most common cause of Dependency cycles is an SRP violation. Fixing the violation by breaking classes into smaller, more focused classes is typically a good solution, but there are also other strategies for breaking Dependency cycles.
6.3.4 打破依赖循环的常用策略
6.3.4 Common strategies for breaking Dependency cycles
When we encounter a Dependency cycle, our first question is, “Where did I fail?” A Dependency cycle should immediately trigger a thorough evaluation of the root cause. Any cycle is a design smell, so your first reaction should be to redesign the involved part to prevent the cycle from happening in the first place. Table 6.4 shows some general directions you can take.
Make no mistake: a Dependency cycle is a design smell. Your first priority should be to analyze the code to understand why the cycle appears. Still, sometimes you can’t change the design, even if you understand the root cause of the cycle.
6.3.5 最后的手段:用属性注入打破循环
6.3.5 Last resort: Breaking the cycle with Property Injection
In some cases, the design error is out of your control, but you still need to break the cycle. In such cases, you can do this by using Property Injection, even if it’s a temporary solution.
To break the cycle, you must analyze it to figure out where you can make a cut. Because using Property Injection suggests an optional rather than a required Dependency, it’s important that you closely inspect all Dependencies to determine where cutting hurts the least.
In our audit trail example, you can resolve the cycle by changing the Dependency of SqlAuditTrailAppender from Constructor Injection to Property Injection. This means that you can create SqlAuditTrailAppender first, inject it into SqlUserRepository, and then subsequently assign AspNetUserContextAdapter to SqlAuditTrailAppender, as this listing shows.
Listing 6.29 Breaking a Dependency cycle with Property Injection
var appender =
new SqlAuditTrailAppender(context, timeProvider); ①
var repository =
new SqlUserRepository(context, appender); ②
var userContext = new
AspNetUserContextAdapter(
new SqlUserByNameRetriever(context));
appender.UserContext = userContext; ③
Using Property Injection this way adds extra complexity to SqlAuditTrailAppender, because it must now be able to deal with a Dependency that isn’t yet available. This leads to Temporal Coupling, as discussed in section 4.3.2.
If you don’t want to relax any of the original classes in this way, a closely related approach is to introduce a Virtual Proxy, which leaves SqlAuditTrailAppender intact:17
Listing 6.30 Breaking a Dependency cycle with a Virtual Proxy
var lazyAppender = new LazyAuditTrailAppender(); ①
var repository =
new SqlUserRepository(context, lazyAppender);
var userContext = new
AspNetUserContextAdapter(
new SqlUserByNameRetriever(context));
lazyAppender.Appender = ②
new SqlAuditTrailAppender(
userContext, context, timeProvider);
LazyAuditTrailAppender implements IAuditTrailAppender like SqlAuditTrailAppender does. But it takes its IAuditTrailAppenderDependency through Property Injection instead of Constructor Injection, allowing you to break the cycle without violating the invariants of the original classes. The next listing shows the LazyAuditTrailAppender Virtual Proxy.
Listing 6.31 A LazyAuditTrailAppender Virtual Proxy implementation
public class LazyAuditTrailAppender : IAuditTrailAppender
{
public IAuditTrailAppender Appender { get; set; } ①
public void Append(Entity changedEntity)
{
if (this.Appender == null) ②
{
throw new InvalidOperationException("Appender was not set.");
}
this.Appender.Append(changedEntity); ③
}
}
Always keep in mind that the best way to address a cycle is to redesign the API so that the cycle disappears. But in the rare cases where this is impossible or highly undesirable, you must break the cycle by using Property Injection in at least one place. This enables you to compose the rest of the object graph apart from the Dependency associated with the property. When the rest of the object graph is fully populated, you can inject the appropriate instance via the property. Property Injection signals that a Dependency is optional, so you shouldn’t make the change lightly.
当您了解一些基本原理时,DI 并不是特别困难。然而,在您学习的过程中,您肯定会遇到可能让您困惑一段时间的问题。本章讨论了人们遇到的一些最常见的问题。它与前两章一起构成了模式、反模式和代码味道的目录。该目录构成本书的第 2 部分。在第 3 部分中,我们将转向 DI 的三个维度:对象组合、生命周期管理和拦截。
DI isn’t particularly difficult when you understand a few basic principles. As you learn, however, you’re guaranteed to run into issues that may leave you stumped for a while. This chapter addressed some of the most common issues people encounter. Together with the two preceding chapters, it forms a catalog of patterns, anti-patterns, and code smells. This catalog constitutes part 2 of the book. In part 3, we’ll turn toward the three dimensions of DI: Object Composition, Lifetime Management, and Interception.
Ever-changing Abstractions are a clear sign of Single Responsibility Principle (SRP) violations. This also relates to the Open/Closed Principle that states that you should be able to add features without having to change existing classes.
The more methods a class has, the higher the chance it violates the SRP. This is also related to the Interface Segregation Principle, which states that no client should be forced to depend on methods it doesn’t use.
简化抽象可以解决许多问题,例如创建代理、装饰器和测试替身的复杂性。
Making Abstractions thinner solves many problems, such as the complexity of creating Proxies, Decorators, and Test Doubles.
A benefit of Constructor Injection is that it becomes more obvious when you violate the SRP. When a single class has too many Dependencies, it’s a signal that you should redesign it.
当构造函数的参数列表变得太大时,我们将这种现象称为构造函数过度注入,并将其视为代码异味。这是一种与 DI 无关但被 DI 放大的一般代码味道。
When a constructor’s parameter list grows too large, we call the phenomenon Constructor Over-injection and consider it a code smell. It’s a general code smell unrelated to, but magnified by, DI.
You can redesign from Constructor Over-injection in many ways, but splitting up a large class into smaller, more focused classes according to well-known design patterns is always a good move.
您可以通过应用 Facade Services 重构来重构远离构造函数过度注入。Facade Service 隐藏了一个自然的交互依赖集群,它们的行为隐藏在一个Abstraction后面。
You can refactor away from Constructor Over-injection by applying Facade Services refactoring. A Facade Service hides a natural cluster of interacting Dependencies with their behavior behind a single Abstraction.
Facade Service 重构允许发现这些自然集群,并公开绘制以前未发现的关系和领域概念。Facade Service 与参数对象相关,但它不是组合和暴露组件,而是仅暴露封装的行为而隐藏成分。
Facade Service refactoring allows discovering these natural clusters and draws previously undiscovered relationships and domain concepts out in the open. Facade Service is related to Parameter Objects but, instead of combining and exposing components, it exposes only the encapsulated behavior while hiding the constituents.
You can refactor away from Constructor Over-injection by introducing domain events into your application. With domain events, you capture actions that can trigger a change to the state of the application you’re developing.
A Leaky Abstraction is an Abstraction, such as an interface, that leaks implementation details, such as layer-specific types or implementation-specific behavior.
Abstractions that implement IDisposable are Leaky Abstractions. IDisposable should be put into effect within the implementation instead.
从概念上讲,服务抽象只有一个实例。将这些知识泄漏给消费者的抽象在设计时并没有考虑到这些消费者。
Conceptually, there’s only one instance of a service Abstraction. Abstractions that leak this knowledge to their consumers aren’t designed with those consumers in mind.
服务抽象通常不应在其定义中公开其他服务抽象。依赖于其他抽象的抽象迫使他们的客户了解这两个抽象。
Service Abstractions should typically not expose other service Abstractions in their definition. Abstractions that depend on other Abstractions force their clients to know about both Abstractions.
在应用 DI 时,抽象工厂经常被过度使用。在许多情况下,存在更好的选择。
When it comes to applying DI, Abstract Factories are often overused. In many cases, better alternatives exist.
The Dependencies created by an Abstract Factory should conceptually require a runtime value. The translation from a runtime value into an Abstraction should make sense on the conceptual level. If you feel the urge to introduce an Abstract Factory to be able to create instances of a concrete implementation, you may have a Leaky Abstraction on hand. Instead, the Proxy pattern provides you with a better solution.
在某些类中具有类似工厂的行为通常是不可避免的。然而,应用程序范围的工厂抽象应该被怀疑地审查。
Having factory-like behavior inside some classes is typically unavoidable. Application-wide Factory Abstractions, however, should be reviewed with suspicion.
抽象工厂总是会增加消费者所拥有的依赖项的数量及其复杂性。
An Abstract Factory always increases the number of Dependencies a consumer has, along with its complexity.
When you use Abstract Factories to select Dependencies based on supplied runtime data, more often than not, you can reduce complexity by refactoring towards Facades that don’t expose the underlying Dependency.
依赖循环通常是由违反 SRP 引起的。
Dependency cycles are typically caused by SRP violations.
Improving the design of the part of the application that contains the Dependency cycle should be your preferred option. In the majority of cases, this means splitting up classes into smaller, more focused classes.
Dependency cycles can be broken using Property Injection. You should only resort to solving cycles by using Property Injection as a last-ditch effort. It only treats the symptoms instead of curing the illness.
类永远不应在其构造函数中执行涉及依赖项的工作,因为注入的依赖项可能尚未完全初始化。
Classes should never perform work involving Dependencies in their constructors because the injected Dependency may not yet be fully initialized.
第 3 部分
纯 DI
Part 3
Pure DI
我在第 1 章中,我们简要概述了 DI 的三个维度:对象组合、生命周期管理和拦截。在本书的这一部分,我们将深入探讨这些维度,并为每个维度提供自己的章节。许多DI 容器具有与这些维度直接相关的特性。一些提供所有三个维度的功能,而另一些仅支持其中的一部分。
In chapter 1, we gave a short outline of the three dimensions of DI: Object Composition, Lifetime Management, and Interception. In this part of the book, we’ll explore these dimensions in depth, providing each with their own chapter. Many DI Containers have features that directly relate to these dimensions. Some provide features in all three dimensions, whereas others only support some of them.
Because a DI Container is an optional tool, we feel it’s more important to explain the underlying principles and techniques that containers typically use to implement these features. Given this, part 3 examines how to apply DI without using a DI Container at all. A practical do-it-yourself guide, this is what we call Pure DI.
第 7 章解释了如何在各种框架(如 ASP.NET Core MVC、控制台应用程序等)中组合对象。并非所有框架都同样好地支持 DI,即使在支持的框架中,细节也有很大差异。对于每个框架,可能很难识别启用 DI 的Seam。但是,一旦找到该Seam,您就有了适用于所有使用该特定框架的应用程序的解决方案。在第 7 章中,我们为最常见的 .NET 应用程序框架完成了这项工作。将其视为框架Seams的目录。
Chapter 7 explains how to compose objects in various frameworks like ASP.NET Core MVC, Console Applications, and so on. Not all frameworks support DI equally well, and even among those that do, the details differ a lot. For each framework, it can be difficult to identify the Seam that enables DI. Once that Seam is found, however, you have a solution for all applications that use that particular framework. In chapter 7, we’ve done this work for the most common .NET application frameworks. Think of it as a catalog of framework Seams.
Although composing objects isn’t particularly hard with Pure DI, you should begin to see the benefits of a real DI Container after reading about Lifetime Management in chapter 8. It’s possible to properly manage the lifetime of various objects in an object graph, but it requires more custom code than Object Composition. And none of that code adds any particular business value to an application. In addition to explaining the basics of Lifetime Management, chapter 8 also contains a catalog of common lifestyles. This catalog serves as a vocabulary for discussing lifestyles throughout part 4. Although you don’t have to implement any of these by hand, it’s good to know how they work.
第 3 部分的其余章节解释了 DI 的最后一个维度:拦截。在第 9 章中,我们将研究实现中经常出现的问题以基于组件的方式横切关注点。我们将使用装饰器设计模式来做到这一点。第 9 章还作为其后两章的介绍。
The remaining chapters of part 3 explain the last dimension of DI: Interception. In chapter 9, we’ll look at the frequently occurring problem of implementing Cross-Cutting Concerns in a component-based way. We’ll do this by using the Decorator design pattern. Chapter 9 also functions as an introduction to the two chapters following it.
We’ll look at the Aspect-Oriented Programming (AOP) paradigm in chapter 10 and see how a careful application design, based on the SOLID principles, enables you to create highly maintainable code, without the use of any special tooling. We consider this chapter the climax of the book — this is where many readers using the early access program said they began to see the contours of a tremendously powerful way to model software.
Besides applying SOLID design principles, there are other ways to practice Aspect-Oriented Programming. Instead of using patterns and principles, you can use specialized tooling such as compile-time weaving and dynamic Interception tools. These are described in chapter 11.
7
申请组成
7
Application composition
在这一章当中
In this chapter
编写控制台应用程序
Composing console applications
编写通用 Windows 编程 (UWP) 应用程序
Composing Universal Windows Programming (UWP) applications
Cooking a gourmet meal with several courses is a challenging undertaking, particularly if you want to partake in the consumption. You can’t eat and cook at the same time, yet many dishes require last-minute cooking to turn out well. Professional cooks know how to resolve many of these challenges. Amidst many tricks of the trade, they use the general principle of mise en place, which can be loosely translated to everything in place.1 Everything that can be prepared well in advance is, well, prepared in advance. Vegetables are cleaned and chopped, meats cut, stocks cooked, ovens preheated, tools laid out, and so on.
If ice cream is part of the dessert, it can be made the day before. If the first course contains mussels, they can be cleaned hours before. Even such a fragile component as sauce béarnaise can be prepared up to an hour before. When the guests are ready to eat, only the final preparations are necessary: reheat the sauce while frying the meat, and so on. In many cases, this final composition of the meal need not take more than 5 to 10 minutes. Figure 7.1 illustrates the process.
图 7.1 Mise en place包括提前准备好膳食的所有成分,以便尽可能快速、轻松地完成膳食的最终组成。
Figure 7.1Mise en place involves preparing all components of the meal well in advance so that the final composition of the meal can be done as quickly and effortlessly as possible.
mise en place的原则类似于使用 DI 开发松散耦合的应用程序。您可以提前编写所有必需的组件,并且只在绝对必要时才编写它们。
The principle of mise en place is similar to developing a loosely coupled application with DI. You can write all the required components well in advance and only compose them when you absolutely must.
As with all analogies, we can only take this one so far. In cooking, preparation and composition are separated by time, whereas in application development, separation occurs across modules and layers. Figure 7.2 shows how to compose the components in the Composition Root.
在运行时,首先发生的是对象组合。一旦连接了对象图,对象组合就完成了,组成部分接管了。在本章中,我们将重点关注组合根。与mise en place相比,Object Composition不会尽可能晚地发生,而是在需要集成不同模块的地方发生。
At runtime, the first thing that happens is Object Composition. As soon as the object graph is wired up, Object Composition is finished, and the constituent components take over. In this chapter, we’ll focus on the Composition Roots of several application frameworks. In contrast to mise en place, Object Composition doesn’t happen as late as possible, but in a place where integration of the different modules is required.
对象组合是 DI 的基础,也是最容易理解的部分之一。您已经知道该怎么做,因为在创建包含其他对象的对象时,您一直在组合对象。
Object Composition is the foundation of DI, and it’s one of the easiest parts to understand. You already know how to do it because you compose objects all the time when you create objects that contain other objects.
在 4.1 节中,我们介绍了何时以及如何编写应用程序的基础知识。本章不重复该信息。相反,我们希望帮助您解决在组合对象时可能出现的一些挑战。这些挑战并非源于对象组合本身,而是来自您工作的应用程序框架。这些问题往往特定于每个框架,解决方案也是如此。根据我们的经验,这些挑战构成了成功应用 DI 的一些最大障碍,因此我们将重点关注它们。这样做会使本章比前几章理论性更强,实践性更强。
In section 4.1, we covered the basics of when and how to compose applications. This chapter doesn’t repeat that information. Instead, we want to help you address some of the challenges that can arise as you compose objects. Those challenges stem not from Object Composition itself, but from the application frameworks in which you work. These issues tend to be specific to each framework, and so are the resolutions. In our experience, these challenges pose some of the greatest obstacles to successfully applying DI, so we’ll focus on them. Doing so will make the chapter less theoretical and more practical than the previous chapters.
当您可以完全控制应用程序的生命周期(就像您对命令行应用程序所做的那样)时,很容易组成应用程序的整个依赖关系层次结构。但是 .NET 中的某些框架(例如 ASP.NET Core)涉及控制反转,这有时会使应用 DI 变得更加困难。了解每个框架的接缝是为特定框架应用 DI 的关键。在本章中,我们将研究如何实现在最常见的 .NET Core 框架中实现组合根。
It’s easy to compose an application’s entire Dependency hierarchy when you have full control over the application’s lifetime (as you do with command-line applications). But some frameworks in .NET (for example, ASP.NET Core) involve Inversion of Control, which can sometimes make it more difficult to apply DI. Understanding each framework’s Seams is key to applying DI for that particular framework. In this chapter, we’ll examine how to implement Composition Roots in the most common .NET Core frameworks.
我们将以在特定框架中应用 DI 的一般介绍开始每个部分,然后是一个基于电子商务示例的广泛示例,该示例贯穿本书的大部分内容。我们将从应用 DI 的最简单的框架开始,然后逐渐研究更复杂的框架。到目前为止,最容易应用 DI 的类型是控制台应用程序,所以我们接下来将讨论这个。
We’ll begin each section with a general introduction to applying DI in a particular framework, followed by an extensive example built on the e-commerce example that runs throughout most of this book. We’ll start with the easiest framework in which to apply DI, and then gradually work through the more complex frameworks. The easiest type to apply DI to is, by far, a console application, so we’ll discuss this next.
A console application is, hands down, the easiest type of application to compose. Contrary to most other .NET BCL application frameworks, a console application involves virtually no Inversion of Control. When execution hits the application’s entry point (usually the Main method in the Program class), you’re on your own. There are no special events to subscribe to, no interfaces to implement, and precious few services you can use.
The Program class is a suitable Composition Root. In its Main method, you compose the application’s modules and let them take over. There’s nothing to it, but let’s look at an example.
7.1.1 示例:使用 UpdateCurrency 程序更新货币
7.1.1 Example: Updating currencies using the UpdateCurrency program
In chapter 4, we looked at how to provide a currency conversion feature for the sample e-commerce application. Section 4.2.4 introduced the ICurrencyConverterAbstraction that applies exchange rates from one currency to other currencies. Because ICurrencyConverter is an interface, we could have created many different implementations, but in the example, we used a database. The purpose of the example code in chapter 4 was to demonstrate how to retrieve and implement currency conversion, so we never looked at how to update exchange rates in the database.
To continue the example, let’s examine how to write a simple .NET Core console application that enables an administrator or super-user to update the exchange rates without having to interact directly with the database. The console application talks to the database and processes the incoming command-line arguments. Because the purpose of this program is to update the exchange rates in the database, we’ll call it UpdateCurrency. It takes two command-line arguments:
货币代码
The currency code
从主要货币 (USD) 到此货币的汇率
The exchange rate from the primary currency (USD) to this currency
USD is the primary currency in our system, and we store all the exchange rates of other currencies relative it. For example, the exchange rate for USD to EUR is expressed as 1 USD costing 0.88 EUR (December 2018). When we want to update the exchange rate at the command line, it looks like this:
UpdateCurrency uses the default entry point for a console program: the Main method in the Program class. This acts as the Composition Root for the application.
The Program class’s only responsibilities are to load the configuration values, compose all relevant modules, and let the composed object graph take care of the functionality. In this example, the composition of the application’s modules is extracted to the CreateCurrencyParser method, whereas the Main method is responsible for calling methods on the composed object graph. CreateCurrencyParser composes its object graph using hardwired Dependencies. We’ll return to it shortly to examine how it’s implemented.
Any Composition Root should only do four things: load configuration values, build the object graph, invoke the desired functionality, and, as we’ll discuss in the next chapter, release the object graph. As soon as it has done that, it should get out of the way and leave the rest to the invoked instance.
With this infrastructure in place, you can now ask CreateCurrencyParser to create a CurrencyParser that parses the incoming arguments and eventually executes the corresponding command. This example uses Pure DI, but it’s straightforward to replace it with a DI Container like those covered in part 4.
7.1.3 组合对象图CreateCurrencyParser
7.1.3 Composing object graphs in CreateCurrencyParser
The CreateCurrencyParser method exists for the express purpose of wiring up all Dependencies for the UpdateCurrency program. The following listing shows the implementation.
Listing 7.2CreateCurrencyParser method that composes the object graph
static CurrencyParser CreateCurrencyParser(string connectionString)
{
IExchangeRateProvider provider = ① new SqlExchangeRateProvider( ① new CommerceContext(connectionString)); ① ① return new CurrencyParser(provider); ①
}
In this listing, the object graph is rather shallow. The CurrencyParser class requires an instance of the IExchangeRateProvider interface, and you construct SqlExchangeRateProvider for communicating with the database in the CreateCurrencyParser method.
The CurrencyParser class uses Constructor Injection, so you pass it the SqlExchangeRateProvider instance that was just created. You then return the newly created CurrencyParser from the method. In case you’re wondering, here’s the constructor signature of CurrencyParser:
public CurrencyParser(IExchangeRateProvider exchangeRateProvider)
Recall that IExchangeRateProvider is an interface that’s implemented by SqlExchangeRateProvider. As part of the Composition Root, CreateCurrencyParser contains a hard-coded mapping from IExchangeRateProvider to SqlExchangeRateProvider. The rest of the code, however, remains loosely coupled, because it consumes only the Abstraction.
This example may seem simple, but it composes types from three different application layers. Let’s briefly examine how these layers interact in this example.
The Composition Root is where components from all layers are wired together. The entry point and the Composition Root constitute the only code of the executable. All implementation is delegated to lower layers, as figure 7.3 illustrates.
The diagram in figure 7.3 may look complicated, but it represents almost the entire code base of the console application. Most of the application logic consists of parsing the input arguments and choosing the correct command based on the input. All this takes place in the application services layer, which only talks directly with the domain layer via the IExchangeRateProvider interface and the Currency class.
IExchangeRateProviderCurrencyParser由Composition Root注入,随后用作抽象工厂来创建Currency由. 数据访问层提供域抽象的基于 SQL Server 的实现。尽管没有其他应用程序类直接与这些实现对话,但将抽象映射到具体类。UpdateCurrencyCommandCreateCurrencyParser
IExchangeRateProvider is injected into CurrencyParser by the Composition Root and is subsequently used as an Abstract Factory to create a Currency instance used by UpdateCurrencyCommand. The data access layer supplies the SQL Server–based implementations of the domain Abstractions. Although none of the other application classes talk directly to those implementations, CreateCurrencyParser maps the Abstractions to the concrete classes.
将 DI 与控制台应用程序一起使用很容易,因为实际上不涉及外部控制反转。.NET Framework 启动进程并将控制权交给Main方法。这类似于使用通用 Windows 编程 (UWP),它允许没有任何接缝的对象组合。
Using DI with a console application is easy because there’s virtually no external Inversion of Control involved. The .NET Framework spins up the process and hands control to the Main method. This is similar to working with Universal Windows Programming (UWP), which allows Object Composition without any Seams.
Composing a UWP application is almost as easy as composing a console application. In this section, we’ll implement a small UWP application for managing products of the e-commerce application using the Model-View-ViewModel (MVVM) pattern. We’ll take a look at where to place the Composition Root, how to construct and initialize view models, how to bind views to their corresponding view models, and how to ensure we can navigate from one page to the next.
UWP 应用程序的入口点相当简单,尽管它不提供Seams明确以启用 DI 为目标,您可以轻松地以您喜欢的任何方式编写应用程序。
A UWP application’s entry point is fairly uncomplicated, and although it doesn’t provide Seams explicitly targeted at enabling DI, you can easily compose an application in any way you prefer.
A UWP application’s entry point is defined in its App class. As with most other classes in UWP, this class is split into two files: App.xaml and App.xaml.cs. You define what happens at application startup in the App.xaml.cs.
当你在 Visual Studio 中创建一个新的 UWP 项目时,App.xaml.cs 文件定义了一个OnLaunched方法来定义应用程序启动时显示哪个页面;在这种情况下,MainPage。
When you create a new UWP project in Visual Studio, the App.xaml.cs file defines an OnLaunched method that defines which page is shown when the application starts; in this case, MainPage.
The OnLaunched method is similar to a console application’s Main method — it’s the entry point for your application. The App class becomes the application’s Composition Root. You can use a DI Container or Pure DI to compose the page; the next example uses Pure DI.
7.2.2 示例:连接产品管理富客户端
7.2.2 Example: Wiring up a product-management rich client
The example in the previous section created our commerce console application for setting exchange rates. In this example, you’ll create a UWP application that enables you to manage products. Figures 7.4 and 7.5 show screen captures of this application.
Figure 7.4 Product Management’s main page is a list of products. You can edit or delete products by tapping on a row, or you can add a new product by tapping Add Product.
Figure 7.5 Product Management’s product-edit page lets you change the product name and unit price in dollars. The application makes use of UWP’s default command bar.
The entire application is implemented using the MVVM approach and contains the four layers shown in figure 7.6. We keep the part with the most logic isolated from the other modules; in this case, that’s the presentation logic. The UWP client layer is a thin layer that does little apart from defining the UI and delegating implementation to the other modules.
Figure 7.6 The four distinct assemblies of the product-management rich client application
图 7.6中的图表与您在前面章节中看到的类似,只是增加了表示逻辑层。数据访问层可以直接连接到数据库,就像我们在电子商务 Web 应用程序中所做的那样,或者它可以连接到产品管理 Web 服务。信息的存储方式与表示逻辑层无关,因此我们不会在本章中详细介绍。
The diagram in figure 7.6 is similar to what you’ve seen in previous chapters, with the addition of a presentation logic layer. The data access layer can directly connect to a database, as we did in the e-commerce web application, or it can connect to a product-management web service. How the information is stored isn’t that relevant where the presentation logic layer is concerned, so we won’t go into details about that in this chapter.
With MVVM, you assign a ViewModel to a page’s DataContext property, and the data-binding and data-templating engines take care of presenting the data correctly as you spin up new ViewModels or change the data in the existing ViewModels. Before you can create the first ViewModel, however, you need to define some constructs that enable ViewModels to navigate to other ViewModels. Likewise, for a ViewModel to be initialized with the runtime data required when a page is shown to the user, you must let the ViewModels implement a custom interface. The following section addresses these concerns before getting to the meat of the application: the MainViewModel.
MainPage contains only XAML markup and no custom code-behind. Instead, it uses data binding to display data and handle user commands. To enable this, you must assign a MainViewModel to its DataContext property. This, however, is a form of Property Injection. We'd like to use Constructor Injection instead. To allow this, we remove the MainPage’s default constructor with an overloaded constructor that accepts the MainViewModel as an argument, where the constructor internally assigns that DataContext property:
public sealed partial class MainPage : Page
{
public MainPage(MainViewModel vm)
{
this.InitializeComponent();
this.DataContext = vm;
}
}
MainViewModel exposes data, such as the list of products, as well as commands to create, update, or delete a product. Enabling this functionality depends on a service that provides access to the product catalog: the IProductRepositoryAbstraction. Apart from IProductRepository, MainViewModel also needs a service that it can use to control its windowing environment, such as navigating to other pages. This other Dependency is called INavigationService:
public interface INavigationService
{
void NavigateTo<TViewModel>(Action whenDone = null, object model = null)
where TViewModel : IViewModel;
}
The NavigateTo method is generic, so the type of ViewModel that it needs to navigate to must be supplied as its generic type argument. The method arguments are passed by the navigation service to the created ViewModel. For this to work, a ViewModel must implement IViewModel. For this reason, the NavigateTo method specifies the generic type constraint where TViewModel : IViewModel.5 The following code snippet shows IViewModel:
public interface IViewModel
{
void Initialize(Action whenDone, object model); ①
}
The Initialize method contains the same arguments as the INavigationService.NavigateTo method. The navigation service will invoke Initialize on a constructed ViewModel. The model represents the data that the ViewModel needs to initialize, such as a Product. The whenDone action allows the originating ViewModel to get notified when the user exits this ViewModel, as we’ll discuss shortly.
The command methods, AddProduct and EditProduct, both instruct INavigationService to navigate to the page for the corresponding ViewModel. In the case of AddProduct, this corresponds to NewProductViewModel. The NavigateTo method is supplied with a delegate that’ll be invoked by NewProductViewModel when the user finishes working on that page. This results in invoking the MainViewModel’s GoBack method, which will navigate the application back to MainViewModel. To paint a complete picture, listing 7.5 shows a simplified version of the MainPage XAML definition and how the XAML is bound to the Model, EditProductCommand, and AddProductCommand properties of MainViewModel.
Although the previous XAML makes use of the older Binding markup extension, as a UWP developer, you might be used to using the newer x:Bind markup extension. x:Bind gives compile-time support, but requires types to be fixed at compile time, typically defined in the view’s code-behind class. Because you bind to a ViewModel that’s stored in the untyped DataContext property, you lose compile-time support and, therefore, need to fall back to the Binding markup extension.6
MainPageXAML 中的两个主要元素是 aGridView和 a CommandBar。GridView用于显示可用产品并绑定到和属性;它绑定到的元素的和属性。这会显示一个通用功能区,其中包含允许用户调用的操作。绑定到属性ModelEditProductCommandDataTemplateNameUnitPriceModelProductCommandBarCommandBarAddProductCommand. 使用 和 的定义MainViewModel,MainPage您现在可以开始连接应用程序。
The two main elements in the MainPage XAML are a GridView and a CommandBar. The GridView is used to display the available products and bind to both the Model and EditProductCommand properties; its DataTemplate binds to the Name and UnitPrice properties of the Model’s Product elements. The CommandBar displays a generic ribbon with operations that the user is allowed to invoke. The CommandBar binds to the AddProductCommand property. With the definitions of MainViewModel and MainPage, you can now start wiring up the application.
Before wiring up MainViewModel, let’s take a look at all the classes involved in this Dependency graph. Figure 7.7 shows the graph for the application, starting with MainPage.
现在您已经确定了应用程序的所有构建块,您可以对其进行组合。为此,您必须同时创建 aMainViewModel和 a MainPage,然后将 ViewModel 注入到MainPage的构造函数中. 要连接起来,您必须将它与它的依赖项组合起来:MainViewModel
Now that you’ve identified all the building blocks of the application, you can compose it. To do this, you must create both a MainViewModel and a MainPage, and then inject the ViewModel to the MainPage’s constructor. To wire up MainViewModel, you have to compose it with its Dependencies:
IViewModel vm = new MainViewModel(navigationService, productRepository);
Page view = new MainPage(vm);
正如您在清单 7.3中看到的,默认的 Visual Studio 模板调用Frame.Navigate(Type). Navigate方法_Page代表您创建一个新实例并将该页面显示给用户。无法向 提供Page实例Navigate,但您可以通过手动将创建的页面分配给Content属性来解决此问题应用程序的主要部分Frame:
As you saw in listing 7.3, the default Visual Studio template calls Frame.Navigate(Type). The Navigate method creates a new Page instance on your behalf and shows that page to the user. There’s no way to supply a Page instance to Navigate, but you can work around this by manually assigning the page created to the Content property of the application’s main Frame:
var frame = (Frame)Window.Current.Content;
frame.Content = view;
There are many ways to create the Composition Root. For this example, we chose to place both the navigation logic and the construction of View/ViewModel pairs inside the App.xaml.cs file to keep the example relatively succinct. The application’s Composition Root is displayed in figure 7.8 .
Listing 7.6 The product-management App class containing the Composition Root
public sealed partial class App : Application, INavigationService
{
protected override void OnLaunched( ①
LaunchActivatedEventArgs e)
{
if (Window.Current.Content == null)
{
Window.Current.Content = new Frame(); ②
Window.Current.Activate();
this.NavigateTo<MainViewModel>(null, null); ③
}
}
public void NavigateTo<TViewModel>(
Action whenDone, object model)
where TViewModel : IViewModel
{
var page = this.CreatePage(typeof(TViewModel)); ④ var viewModel = (IViewModel)page.DataContext; ④ ④ viewModel.Initialize(whenDone, model); ④ ④ var frame = (Frame)Window.Current.Content; ④ frame.Content = page; ④
}
private Page CreatePage(Type vmType)
{
var repository = new WcfProductRepository(); ⑤ ⑤ if (vmType == typeof(MainViewModel)) ⑤ { ⑤ return new MainPage( ⑤ new MainViewModel(this, repository)); ⑤ } ⑤ else if (vmType == typeof(EditProductViewModel)) ⑤ { ⑤ return new EditProductPage( ⑤ new EditProductViewModel(repository)); ⑤ } ⑤ else if (vmType == typeof(NewProductViewModel)) ⑤ { ⑤ return new NewProductPage( ⑤ new NewProductViewModel(repository)); ⑤ { ⑤
else
{
throw new Exception(“Unknown view model.”);
}
...
}
工厂方法类似于我们在 4.1 节中讨论的Composition Root示例。它由一大串语句组成,以相应地构建正确的对。CreatePageelse if
The CreatePage factory method is similar to the Composition Root examples we discussed in section 4.1. It consists of a big list of else if statements to construct the correct pair accordingly.
UWP offers a simple place for a Composition Root. All you need to do is remove the call to Frame.Navigate(Type) from OnLaunched and set Frame.Content with a manually created Page class, which is composed using a ViewModel and its Dependencies.
在大多数其他框架中,存在更高程度的控制反转,这意味着我们需要能够识别正确的扩展点以连接所需的对象图。一种这样的框架是 ASP.NET Core MVC。
In most other frameworks, there’s a higher degree of Inversion of Control, which means we need to be able to identify the correct extensibility points to wire up the desired object graph. One such framework is ASP.NET Core MVC.
7.3 编写 ASP.NET Core MVC 应用程序
7.3 Composing ASP.NET Core MVC applications
ASP.NET Core MVC 是为支持 DI 而构建和设计的。它带有自己的内部组合引擎,您可以使用它来构建自己的组件;不过,正如您将看到的,它并不强制您的应用程序组件使用DI 容器。您可以使用纯 DI或您喜欢的任何DI 容器。7
ASP.NET Core MVC was built and designed to support DI. It comes with its own internal composition engine that you can use to build up its own components; although, as you’ll see, it doesn’t enforce the use of a DI Container for your application components. You can use Pure DI or whichever DI Container you like.7
在本节中,您将学习如何使用 ASP.NET Core MVC 的主要扩展点,它允许您插入逻辑来组合控制器类及其依赖项。本节从 DI对象组合的角度来看 ASP.NET Core MVC 。然而,构建 ASP.NET Core 应用程序的内容远远超过我们在一章中可以解决的问题。如果您想了解有关如何使用 ASP.NET Core 构建应用程序的更多信息,请查看 Andrew Lock 的ASP.NET Core 实战(Manning,2018 年)。之后,我们将看看如何插入需要Dependencies的自定义中间件。
In this section, you’ll learn how to use the main extensibility point of ASP.NET Core MVC, which allows you to plug in your logic for composing controller classes with their Dependencies. This section looks at ASP.NET Core MVC from the perspective of DI Object Composition. There’s a lot more to building ASP.NET Core applications than we can address in a single chapter, however. If you want to learn more about how to build applications with ASP.NET Core, take a look at Andrew Lock’s ASP.NET Core in Action (Manning, 2018). After that, we’ll take a look at how to plug in custom middleware that requires Dependencies.
与在应用程序框架中练习 DI 一样,应用它的关键是找到正确的扩展点。在 ASP.NET Core MVC 中,这是一个名为. 图 7.9说明了它是如何融入框架的。IControllerActivator
As is always the case with practicing DI in an application framework, the key to applying it is finding the correct extensibility point. In ASP.NET Core MVC, this is an interface called IControllerActivator. Figure 7.9 illustrates how it fits into the framework.
控制器是 ASP.NET Core MVC 的核心。他们处理请求并确定如何响应。如果您需要查询数据库、验证和保存传入数据、调用域逻辑等,您可以从控制器启动此类操作。控制器不应该自己做这些事情,而是将工作委托给适当的Dependencies。这就是 DI 的用武之地。
Controllers are central to ASP.NET Core MVC. They handle requests and determine how to respond. If you need to query a database, validate and save incoming data, invoke domain logic, and so on, you initiate such actions from a controller. A controller shouldn’t do such things itself, but rather delegate the work to the appropriate Dependencies. This is where DI comes in.
You want to be able to supply Dependencies to a given controller class, ideally by Constructor Injection. This is possible with a custom IControllerActivator.
7.3.1 创建自定义控制器激活器
7.3.1 Creating a custom controller activator
创建自定义控制器激活器并不是特别困难。它需要你实现IControllerActivator接口:
Creating a custom controller activator isn’t particularly difficult. It requires you to implement the IControllerActivator interface:
The Create method provides a ControllerContext that contains information such as the HttpContext and the controller type. This is the method where you get the chance to wire up all required Dependencies and supply them to the controller before returning the instance. You’ll see an example in a moment.
如果您创建了任何需要显式处理的资源,则可以在Release方法时执行此操作叫做。我们将在下一章中详细介绍有关发布组件的信息。确保释放依赖HttpContext.Response.RegisterForDispose项的更实用方法是使用方法将它们添加到可释放请求对象列表中. 尽管实现自定义控制器激活器是困难的部分,但除非我们将其告知 ASP.NET Core MVC,否则不会使用它。
If you created any resources that need to be explicitly disposed of, you can do that when the Release method is called. We’ll go into further details about releasing components in the next chapter. A more practical way to ensure that Dependencies are disposed of is to add them to the list of disposable request objects using the HttpContext.Response.RegisterForDispose method. Although implementing a custom controller activator is the hard part, it won’t be used unless we tell ASP.NET Core MVC about it.
在 ASP.NET Core 中使用自定义控制器激活器
Using a custom controller activator in ASP.NET Core
A custom controller activator can be added as part of the application startup sequence — usually in the Startup class. They’re used by calling AddSingleton<IControllerActivator> on the IServiceCollection instance. The next listing shows the Startup class from the sample e-commerce application.
This listing creates a new instance of the custom CommerceControllerActivator. By adding it to the list of known services using AddSingleton, you ensure the creation of controllers is Intercepted by your custom controller activator. If this code looks vaguely familiar, it’s because you saw something similar in section 4.1.3. Back then, we promised to show you how to implement a custom controller activator in chapter 7, and what do you know? This is chapter 7.
示例:实施CommerceControllerActivator
Example: implementing the CommerceControllerActivator
As you might recall from chapters 2 and 3, the e-commerce sample application presents the visitor of the website with a list of products and their prices. In section 6.2, we added a feature that allowed users to calculate a route between two locations. Although we’ve shown several snippets of the Composition Root, we didn’t show a complete example. Together with listing 7.7’s Startup class, listing 7.8’s CommerceControllerActivator class shows a complete Composition Root.
The e-commerce sample application needs a custom controller activator to wire up controllers with their required Dependencies. Although the entire object graph is considerably deeper, from the perspective of the controllers themselves, the union of all immediate Dependencies is as small as two items (figure 7.10).
Listing 7.8 Creating controllers using a custom controller activator
public class CommerceControllerActivator : IControllerActivator
{
private readonly string connectionString;
public CommerceControllerActivator(string connectionString)
{
this.connectionString = connectionString;
}
public object Create(ControllerContext context)
{
Type type = context.ActionDescriptor ① .ControllerTypeInfo.AsType(); ① if (type == typeof(HomeController)) ② { ② return this.CreateHomeController(); ② } ② else if (type == typeof(RouteController)) ② { ② return this.CreateRouteController(); ②
}
else
{
throw new Exception("Unknown controller " + type.Name);
}
}
private HomeController CreateHomeController()
{
return new HomeController( ③ new ProductService( ③ new SqlProductRepository( ③ new CommerceContext( ③ this.connectionString)), ③ new AspNetUserContextAdapter())); ③
}
private RouteController CreateRouteController()
{
var routeAlgorithms = ...; ③ return new RouteController( ③ new RouteCalculator(routeAlgorithms)); ③
}
public void Release( ④ ControllerContext context, object controller) ④ { ④ } ④
}
当一个实例在 中注册时,它会正确地创建具有所需依赖项的所有请求的控制器。除了控制器之外,其他经常需要使用 DI 的常见组件是 ASP.NET Core 所说的中间件。CommerceControllerActivatorStartup
When a CommerceControllerActivator instance is registered in Startup, it correctly creates all requested controllers with the required Dependencies. Besides controllers, other common components that often require the use of DI are what ASP.NET Core calls middleware.
7.3.2 使用Pure DI构建自定义中间件组件
7.3.2 Constructing custom middleware components using Pure DI
ASP.NET Core 使得在请求管道中插入额外行为变得相对容易。这种行为会影响请求和响应。在 ASP.NET Core 中,这些对请求管道的扩展称为中间件。将中间件连接到请求管道的典型用法是通过Use扩展方法:
ASP.NET Core makes it relatively easy to plug in extra behavior in the request pipeline. Such behavior can influence the request and response. In ASP.NET Core, these extensions to the request pipeline are called middleware. A typical use of hooking up middleware to the request pipeline is through the Use extension method:
var logger =
loggerFactory.CreateLogger("Middleware"); ① app.Use(async (context, next) => ②
{
logger.LogInformation("Request started"); ③ await next(); ④ logger.LogInformation("Request ended"); ⑤
});
More often, however, more work needs to be done prior to or after the request’s main logic runs. You might therefore want to extract such middleware logic into its own class. This prevents your Startup class from being cluttered and gives you the opportunity to unit test this logic, should you want to do so. You can extract the body of our previous Use lambda to an Invoke method on a newly created LoggingMiddleware class:
public class LoggingMiddleware
{
private readonly ILogger logger;
public LoggingMiddleware(ILogger logger) ①
{
this.logger = logger;
}
public async Task Invoke( ②
HttpContext context, Func<Task> next)
{
this.logger.LogInformation("Request started");
await next();
this.logger.LogInformation("Request ended");
}
}
With the middleware logic now moved into the LoggingMiddleware class, the Startup configuration can be minimized to the following code:
var logger = loggerFactory.CreateLogger("Middleware");
app.Use(async (context, next) =>
{
var middleware = new LoggingMiddleware(logger); ① await middleware.Invoke(context, next); ②
});
ASP.NET Core MVC 的伟大之处在于它在设计时就考虑到了 DI,因此,在大多数情况下,您只需要知道并使用单个扩展点即可为应用程序启用 DI。对象组合是 DI 的三个重要维度之一(其他维度是生命周期管理和拦截)。
The great thing about ASP.NET Core MVC is that it was designed with DI in mind, so, for the most part, you only need to know and use a single extensibility point to enable DI for an application. Object Composition is one of three important dimensions of DI (the others being Lifetime Management and Interception).
在本章中,我们向您展示了如何在各种不同的环境中使用松散耦合的模块来组合应用程序。一些框架实际上使它变得容易。当您编写控制台应用程序和 Windows 客户端(例如 UWP)时,您或多或少可以直接控制应用程序入口点发生的事情。这为您提供了一个独特且易于实现的Composition Root。其他框架,例如 ASP.NET Core,让您的工作更辛苦一些,但它们仍然提供Seams,您可以使用它来定义应用程序的组成方式。ASP.NET Core 在设计时考虑到了 DI,因此编写应用程序就像实现自定义IControllerActivator并将其添加到框架一样简单。
In this chapter, we’ve shown you how to compose applications from loosely coupled modules in a variety of different environments. Some frameworks actually make it easy. When you’re writing console applications and Windows clients (such as UWP), you’re more or less in direct control of what’s happening at the application’s entry point. This provides you with a distinct and easily implemented Composition Root. Other frameworks, such as ASP.NET Core, make you work a little harder, but they still provide Seams you can use to define how the application should be composed. ASP.NET Core was designed with DI in mind, so composing an application is as easy as implementing a custom IControllerActivator and adding it to the framework.
Without Object Composition, there’s no DI, but you may not yet have fully realized the implications for Object Lifetime when we move the creation of objects out of the consuming classes. You may find it self evident that the external caller (often a DI Container) creates new instances of Dependencies — but when are injected instances deallocated? And what if the external caller doesn’t create new instances each time, but instead hands you an existing instance? These are topics for the next chapter.
概括
Summary
对象组合是建立相关组件层次结构的行为,它发生在Composition Root内部。
Object Composition is the act of building up hierarchies of related components, which takes place inside the Composition Root.
组合根应该只做四件事:加载配置值、构建对象图、调用所需的功能以及释放对象图。
A Composition Root should only do four things: load configuration values, build object graphs, invoke the desired functionality, and release the object graphs.
只有Composition Root应该依赖配置文件,因为它更灵活,可以由调用者强制配置库。
Only the Composition Root should rely on configuration files because it’s more flexible for libraries to be imperatively configurable by their callers.
将配置值的加载与执行对象组合的方法分开。这使得在不存在配置文件的情况下测试对象组合成为可能。
Separate the loading of configuration values from the methods that do Object Composition. This makes it possible to test Object Composition without the existence of a configuration file.
Model View ViewModel (MVVM) 是一种设计,其中 ViewModel 是视图和模型之间的桥梁。每个 ViewModel 都是一个以特定技术方式转换和公开模型的类。在 MVVM 中,ViewModel 是将使用 DI 组合的应用程序组件。
Model View ViewModel (MVVM) is a design in which the ViewModel is the bridge between the view and the model. Each ViewModel is a class that translates and exposes the model in a technology-specific way. In MVVM, ViewModels are the application components that will be composed using DI.
在控制台应用程序中,Program该类是合适的Composition Root。
In a console application, the Program class is a suitable Composition Root.
In a UWP application, the App class is a suitable Composition Root, and its OnLaunched method is the main entry point.
在 ASP.NET Core MVC 应用程序中,IControllerActivator是插入Object Composition的正确扩展点。
In an ASP.NET Core MVC application, the IControllerActivator is the correct extensibility point to plug in Object Composition.
确保在 ASP.NET Core 中释放依赖HttpContext.Response.RegisterForDispose项的一种实用方法是使用该方法将它们添加到可释放请求对象列表中。
A practical way to ensure that Dependencies are disposed of in ASP.NET Core is to use the HttpContext.Response.RegisterForDispose method to add them to the list of disposable request objects.
通过将函数注册到实现Composition Root一小部分的管道,可以将中间件添加到 ASP.NET Core 。这组成了中间件组件并调用它。
Middleware can be added to ASP.NET Core by registering a function to the pipeline that implements a small part of the Composition Root. This composes the middleware component and invokes it.
The passing of time has a profound effect on most food and drink, but the consequences vary. Personally, we find 12-month-old Gruyère more interesting than 6-month-old Gruyère, but Mark prefers his asparagus fresher than either of those.1 In many cases, it’s easy to assess the proper age of an item; but in certain cases, doing so becomes complex. This is most notable when it comes to wine (see figure 8.1).
Wines tend to get better with age — until they suddenly become too old and lose most of their flavor. This depends on many factors, including the origin and vintage of the wine. Although wines interest us, we don’t ever expect we’ll be able to predict when a wine will peak. For that, we rely on experts: books at home and sommeliers at restaurants. They understand wines better than we do, so we happily let them take control.
Figure 8.1 Wine, cheese, and asparagus. Although the combination may be a bit off, their age greatly affects their overall qualities.
除非您不阅读任何前面的内容而直接进入本章,否则您就会知道放开控制是 DI 中的一个关键概念。这源于控制反转原则,您将对依赖项的控制委托给第三方,但它也意味着不仅仅是让其他人选择所需抽象的实现。当您允许Composer提供Dependency时,您还必须接受您无法控制它的生命周期。
Unless you dove straight into this chapter without reading any of the previous ones, you know that letting go of control is a key concept in DI. This stems from the Inversion of Control principle, where you delegate control of your Dependencies to a third party, but it also implies more than just letting someone else pick an implementation of a required Abstraction. When you allow a Composer to supply a Dependency, you must also accept that you can’t control its lifetime.
Just as the sommelier intimately knows the contents of the restaurant’s wine cellar and can make a far more informed decision than we can, we should trust the Composer to be able to control the lifetime of Dependencies more efficiently than the consumer. Composing and managing components is its single responsibility.
In this chapter, we’ll explore Dependency Lifetime Management. Understanding this topic is important because, just as you can have a subpar experience if you drink a wine at the wrong age (both your own age and the wine’s), you can experience degraded performance from configuring Dependency Lifetime incorrectly. Even worse, you may get the Lifetime Management equivalent of spoiled food: resource leaks. Understanding the principles of correctly managing the lifecycles of components should enable you to make informed decisions to configure your applications correctly.
We’ll start with a general introduction to Dependency Lifetime Management, followed by a discussion about disposable Dependencies. This first part of the chapter is meant to provide all the background information and guiding principles you need in order to make knowledgeable decisions about your own applications' lifecycles, scope, and configurations.
After that, we’ll look at different lifetime strategies. This part of the chapter takes the form of a catalog of available Lifestyles. In most cases, one of these stock Lifestyle patterns will provide a good match for a given challenge, so understanding them in advance equips you to deal with many difficult situations.
我们将以一些关于生命周期管理的坏习惯或反模式来结束本章。当我们完成后,您应该很好地掌握了终身管理和常见生活方式的注意事项。首先,让我们看看对象生命周期以及它与一般 DI 的关系。
We’ll finish the chapter with some bad habits, or anti-patterns, concerning Lifetime Management. When we’re finished, you should have a good grasp of Lifetime Management and common Lifestyle do’s and don’ts. First, let’s look at Object Lifetime and how it relates to DI in general.
8.1 管理依赖生命周期
8.1 Managing Dependency Lifetime
到目前为止,我们主要讨论了 DI 如何使您能够组合Dependencies。前一章非常详细地探讨了这个主题,但是,正如我们在 1.4 节中提到的,对象组合只是 DI 的一个方面。管理对象生命周期是另一回事。
Up to this point, we’ve mostly discussed how DI enables you to compose Dependencies. The previous chapter explored this subject in great detail, but, as we alluded to in section 1.4, Object Composition is just one aspect of DI. Managing Object Lifetime is another.
我们第一次了解到 DI 的范围包括Lifetime Management的想法时,我们未能理解Object Composition和Object Lifetime之间的深层联系。终于搞定了,而且很简单,一起来看看吧!
The first time we were introduced to the idea that the scope of DI includes Lifetime Management, we failed to understand the deep connection between Object Composition and Object Lifetime. We finally got it, and it’s simple, so let’s take a look!
In this section, we’ll introduce Lifetime Management and how it applies to Dependencies. We’ll look at the general case of composing objects and how it has implications for the lifetimes of Dependencies. First, we’ll investigate why Object Composition implies Lifetime Management.
8.1.1 引入生命周期管理
8.1.1 Introducing Lifetime Management
当我们接受我们应该放弃对依赖项的控制的心理需求,而是通过构造函数注入或其他 DI 模式之一请求它们时,我们必须完全放手。要了解原因,我们将检查问题逐步解决。让我们首先回顾一下标准 .NET 对象生命周期对Dependencies的意义。您可能已经知道这一点,但在我们建立上下文时请耐心等待下半页。
When we accept that we should let go of our psychological need for control over Dependencies and instead request them through Constructor Injection or one of the other DI patterns, we must let go completely. To understand why, we’ll examine the issue progressively. Let’s begin by reviewing what the standard .NET object lifecycle means for Dependencies. You likely already know this, but bear with us for the next half page while we establish the context.
简单的依赖生命周期
Simple Dependency lifecycle
您知道 DI 意味着您让第三方(通常是我们的Composition Root)提供您需要的依赖项。这也意味着您必须让它管理Dependencies的生命周期。当涉及到对象创建时,这是最容易理解的。这是来自示例电子商务应用程序的Composition Root的(稍微重组的)代码片段。(你可以在清单 7.8 中看到完整的例子。)
You know that DI means you let a third party (typically our Composition Root) serve the Dependencies you need. This also means you must let it manage the Dependencies’ lifetimes. This is easiest to understand when it comes to object creation. Here’s a (slightly restructured) code fragment from the sample e-commerce application’s Composition Root. (You can see the complete example in listing 7.8.)
var productRepository =
new SqlProductRepository(
new CommerceContext(connectionString));
var productService =
new ProductService(
productRepository,
userContext);
We hope that it’s evident that the ProductService class doesn’t control when productRepository is created. In this case, SqlProductRepository is likely to be created within the same millisecond; but as a thought experiment, we could insert a call to Thread.Sleep between these two lines of code to demonstrate that you can arbitrarily separate them over time. That would be a pretty weird thing to do, but the point is that not all objects of a Dependency graph have to be created at the same time.
Consumers don’t control creation of their Dependencies, but what about destruction? As a general rule, you don’t control when objects are destroyed in .NET. The garbage collector cleans up unused objects, but unless you’re dealing with disposable objects, you can’t explicitly destroy an object.
Objects are eligible for garbage collection when they go out of scope. Conversely, they last as long as someone else holds a reference to them. Although a consumer can’t explicitly destroy an object — that’s up to the garbage collector — it can keep the object alive by holding on to the reference. This is what you do when you use Constructor Injection, because you save the Dependency in a private field:
public class HomeController
{
private readonly IProductService service;
public HomeController(IProductService service) ①
{
this.service = service; ②
}
}
This means that when the consumer goes out of scope, so can the Dependency. Even when a consumer goes out of scope, however, the Dependency can live on if other objects hold a reference to it. Otherwise, it’ll be garbage collected. Because you’re an experienced .NET developer, this is probably old news to you, but now the discussion should begin to get more interesting.
Until now our analysis of the Dependency lifecycle has been mundane, but now we can add some complexity. What happens when more than one consumer requires the same Dependency? One option is to supply each consumer their own instance, as shown in figure 8.2.
Listing 8.1 Composing with multiple instances of the same Dependency
var repository1 = new SqlProductRepository(connString); ① var repository2 = new SqlProductRepository(connString); ① var productService = new ProductService(repository1); ② var calculator = new DiscountCalculator(repository2); ③
When it comes to the lifecycles of each Repository in listing 8.1, nothing has changed compared to the previously discussed sample e-commerce application’s Composition Root. Each Dependency goes out of scope and is garbage-collected when its consumers go out of scope. This can happen at different times, but the situation is only marginally different than before. It would be a somewhat different situation if both consumers were to share the same Dependency, as shown in figure 8.3.
Listing 8.2 Composing with a single instance of the same Dependency
var repository = new SqlProductRepository(connString);
var productService = new ProductService(repository); ① var calculator = new DiscountCalculator(repository); ①
When comparing listings 8.1 and 8.2, you don’t find that one is inherently better than the other. As we’ll discuss in section 8.3, there are several factors to consider when it comes to when and how you want to reuse a Dependency.
The lifecycle for the Repository Dependency has changed distinctly, compared with the previous example. Both consumers must go out of scope before the variable repository can be eligible for garbage collection, and they can do so at different times. The situation becomes less predictable when the Dependency reaches the end of its lifetime. This trait is only reinforced when the number of consumers increases.
Given enough consumers, it’s likely that there’ll always be one around to keep the Dependency alive. This may sound like a problem, but it rarely is: instead of a multitude of similar instances, you have only one, which saves memory. This is such a desirable quality that we formalize it in a Lifestyle pattern called the Singleton Lifestyle. Don’t confuse this with the Singleton design pattern, although there are similarities.2 We’ll go into greater detail about this subject in section 8.3.1.
The key point to appreciate is that the Composer has a greater degree of influence over the lifetime of Dependencies than any single consumer. The Composer decides when instances are created, and by its choice of whether to share instances, it determines whether a Dependency goes out of scope with a single consumer, or whether all consumers must go out of scope before the Dependency can be released.
This is comparable to visiting a restaurant with a good sommelier. The sommelier spends a large proportion of the day managing and evolving the wine cellar: buying new wines, sampling the available bottles to track how they develop, and working with the chefs to identify optimal matches to the food being served. When we’re presented with the wine list, it includes only what the sommelier deems fit to offer for today’s menu. We’re free to select a wine according to our personal taste, but we don’t presume to know more about the restaurant’s selection of wines and how they go with the food than the sommelier does. The sommelier will often decide to keep lots of bottles in stock for years; and as you’ll see in the next section, a Composer may decide to keep instances alive by holding on to their references.
The previous section explained how you can vary the composition of Dependencies to influence their lifetimes. In this section, we’ll look at how to implement this using Pure DI, while applying the two most commonly used Lifestyles: Transient and Singleton.
在第 7 章中,您创建了专门的类来组合应用程序。其中之一是用于 ASP.NET Core MVC 应用程序——我们的Composer。清单 7.8 显示了其方法的实现。CommerceControllerActivatorCreate
In chapter 7, you created specialized classes to compose applications. One of these was a CommerceControllerActivator for an ASP.NET Core MVC application — our Composer. Listing 7.8 shows the implementation of its Create method.
As you may recall, the Create method creates the entire object graph on the fly each time it’s invoked. Each Dependency is private to the issued controller, and there’s no sharing. When the controller instance goes out of scope (which it does every time the server has replied to a request), all the Dependencies go out of scope too. This is often called a Transient Lifestyle, which we’ll talk more about in section 8.3.2.
Let’s analyze the object graphs created by the CommerceControllerActivator and shown in figure 8.4 to see if there’s room for improvement. Both the AspNetUserContextAdapter and RouteCalculator classes are completely stateless services, so there’s no reason to create a new instance every time you need to service a request. The connection string is also unlikely to change, so you can reuse it across requests. The SqlProductRepository class, on the other hand, relies on an Entity Framework DbContext (implemented by our CommerceContext), which mustn’t be shared across requests.3
Given this particular configuration, a better implementation of CommerceControllerActivator would reuse the same instances of both AspNetUserContextAdapter and RouteCalculator, while creating new instances of ProductService and SqlProductRepository. In short, you should configure AspNetUserContextAdapter and RouteCalculator to use the Singleton Lifestyle, and ProductService and SqlProductRepository as Transient. The following listing shows how to implement this change.
Listing 8.3 Managing lifetime within the CommerceControllerActivator
public class CommerceControllerActivator : IControllerActivator
{
private readonly string connectionString;
private readonly IUserContext userContext; ① private readonly RouteCalculator calculator; ①
public CommerceControllerActivator(string connectionString)
{
this.connectionString = connectionString;
this.userContext = ② new AspNetUserContextAdapter(); ② ② this.calculator = ② new RouteCalculator( ② this.CreateRouteAlgorithms()); ②
}
public object Create(ControllerContext context)
{
Type type = context.ActionDescriptor
.ControllerTypeInfo.AsType();
switch (type.Name)
{
case "HomeController":
return this.CreateHomeController();
case "RouteController":
return this.CreateRouteController();
default:
throw new Exception("Unknown controller " + type.Name);
}
}
private HomeController CreateHomeController()
{
return new HomeController( ③ new ProductService( ③ new SqlProductRepository( ③ new CommerceContext( ③ this.connectionString)), ③ this.userContext)); ③ } ③ ③ private RouteController CreateRouteController() ③ { ③ return new RouteController(this.calculator); ③
}
public void Release(ControllerContext context, ④ object controller) { ... } ④
}
In an MVC application, it’s practical to load configuration values in the Startup class. That’s why in listing 8.3, the connection string is supplied to the constructor of the CommerceControllerActivator.
The code in listing 8.3 is functionally equivalent to the code in listing 7.8 — it’s just slightly more efficient because some of the Dependencies are shared. By holding on to the Dependencies you create, you can keep them alive for as long as you want. In this example, CommerceControllerActivator created both Singleton Dependencies as soon as it was initialized, but it could also have used lazy initialization.
The ability to fine-tune each Dependency’s Lifestyle can be important for performance reasons, but can also be important for correct behavior. For instance, the Mediator design pattern relies on a shared director through which several components communicate.4 This only works when the Mediator is shared among the involved collaborators.
So far, we’ve discussed how Inversion of Control implies that consumers can’t manage the lifetimes of their Dependencies, because they don’t control creation of objects; and because .NET uses garbage collection, consumers can’t explicitly destroy objects, either. This leaves a question unanswered: what about disposable Dependencies? We’ll now turn our attention to that delicate question.
Although .NET is a managed platform with a garbage collector, it can still interact with unmanaged code. When this happens, .NET code interacts with unmanaged memory that isn’t garbage-collected. To prevent memory leaks, you must have a mechanism with which to deterministically release unmanaged memory. This is the key purpose of the IDisposable interface.
It’s likely that some Dependency implementations will contain unmanaged resources. As an example, ADO.NET connections are disposable because they tend to use unmanaged memory. As a result, database-related implementations like Repositories backed by databases are likely to be disposable themselves. How should we model disposable Dependencies? Should we also let Abstractions be disposable? That might look like this:
If you feel the urge to add IDisposable to your interface, it’s probably because you have a particular implementation in mind. But you must not let that knowledge leak through to the interface design. Doing so would make it more difficult for other classes to implement the interface and would introduce vagueness into the Abstraction.
谁负责处理一次性依赖项?会不会是消费者?
Who’s responsible for disposing of a disposable Dependency? Could it be the consumer?
8.2.1 消费一次性依赖
8.2.1 Consuming disposable Dependencies
为了争论,假设你有一个一次性的抽象,如下面的IOrderRepository接口.
For the sake of argument, imagine that you have a disposable Abstraction like the following IOrderRepository interface.
OrderService一堂课应该怎样处理这样的依赖?大多数设计指南(包括 Visual Studio 的内置代码分析)都坚持认为,如果一个类作为成员持有一次性资源,它应该自己实现IDisposable和处置该资源。下一个清单显示了如何。
How should an OrderService class deal with such a Dependency? Most design guidelines (including Visual Studio’s built-in Code Analysis) would insist that if a class holds a disposable resource as a member, it should itself implement IDisposable and dispose of the resource. The next listing shows how.
Listing 8.5OrderService depending on disposable Dependency
public sealed class OrderService : IDisposable ①
{
private readonly IOrderRepository repository;
public OrderService(IOrderRepository repository)
{
this.repository = repository;
}
public void Dispose()
{
this.repository.Dispose(); ②
}
}
但事实证明这是一个坏主意,因为repository成员最初是注入的,它可以被其他消费者共享:
But this turns out to be a bad idea because the repository member was originally injected, and it can be shared by other consumers:
var repository =
new SqlOrderRepository(connectionString);
var validator = new OrderValidator(repository); ① var orderService = new OrderService(repository); ①
orderService.AcceptOrder(order);
orderService.Dispose(); ② validator.Validate(order); ③
It would be less dangerous not to dispose of the injected Repository, but this means you’re ignoring the fact that the Abstraction is disposable. Besides, in this case, the Abstraction exposes more members than used by the client, which is an Interface Segregation Principle violation (see section 6.2.1). Declaring an Abstraction as deriving from IDisposable provides no benefit.
Then again, there can be scenarios where you need to signal the beginning and end of a short-lived scope; IDisposable is sometimes used for that purpose. Before we examine how a Composer can manage the lifetime of a disposable Dependency, we should consider how to deal with such ephemeral disposables.
创建临时一次性用品
Creating ephemeral disposables
.NET BCL 中的许多 API 用于IDisposable发出特定范围已结束的信号。一个更突出的例子是 WCF 代理。
Many APIs in the .NET BCL use IDisposable to signal that a particular scope has ended. One of the more prominent examples is WCF proxies.
It’s important to remember that the use of IDisposable for such purposes need not indicate a Leaky Abstraction, because these types aren’t always Abstractions in the first place. On the other hand, some of them are; and when that’s the case, how do you deal with them?
幸运的是,一个对象被销毁后,你就不能再使用它了。如果你想再次调用相同的 API,你必须创建一个新的实例。作为一个非常适合您如何使用 WCF 代理或 ADO.NET 命令的示例,您创建代理,调用它的操作,并在完成后立即处理它。如果您认为一次性抽象是有漏洞的抽象,您如何将其与 DI 协调起来?
Fortunately, after an object is disposed of, you can’t reuse it. If you want to invoke the same API again, you must create a new instance. As an example that fits well with how you use WCF proxies or ADO.NET commands, you create the proxy, invoke its operations, and dispose of it as soon as you’re finished. How can you reconcile this with DI if you consider disposable Abstractions to be Leaky Abstractions?
与往常一样,将混乱的细节隐藏在界面后面可能会有所帮助。回到第 7.2 节的 UWP 应用程序,我们使用IProductRepository抽象从表示逻辑层隐藏与数据存储通信的细节。在此讨论期间,我们忽略了此类实现的细节,因为它在当时并不那么重要。但我们假设 UWP 应用程序必须与 WCF Web 服务通信。来自EditProductViewModel的透视,这是删除产品的方式:
As always, hiding the messy details behind an interface can be helpful. Returning to the UWP application from section 7.2, we used an IProductRepositoryAbstraction to hide the details of communicating with a data store from the presentation logic layer. During this discussion, we ignored the details of such an implementation because it wasn’t that relevant at that moment. But let’s assume that the UWP application must communicate with a WCF web service. From the EditProductViewModel’s perspective, this is how you delete a product:
private void DeleteProduct()
{
this.productRepository.Delete(this.Model.Id); ①
this.whenDone();
}
Another picture forms when we look at the WCF implementation of that interface. Here’s the implementation of WcfProductRepository with its Delete method.
Listing 8.6 Using a WCF channel as an ephemeral disposable
public class WcfProductRepository : IProductRepository
{
private readonly ChannelFactory<IProductManagementService> factory;
public WcfProductRepository(
ChannelFactory<IProductManagementService> factory)
{
this.factory = factory;
}
public void Delete(Guid productId) ① { ① using (var channel = ① this.factory.CreateChannel()) ① { ① channel.DeleteProduct(productId); ① } ①
}
...
}
WcfProductRepository班级_没有可变状态,因此您可以注入一个ChannelFactory<TChannel>可用于创建通道的。通道只是 WCF 代理的另一个词,它是您在使用 Visual Studio 或 svcutil.exe 创建服务引用时免费获得的自动生成的客户端界面。
The WcfProductRepository class has no mutable state, so you inject a ChannelFactory<TChannel> that you can use to create a channel. Channel is just another word for a WCF proxy, and it’s the autogenerated client interface you get for free when you create a service reference with Visual Studio or svcutil.exe.
Because this interface derives from IDisposable, you can wrap it in a using statement. You then use the channel to delete the product. When you exit the using scope, the channel is disposed of.
Every time you invoke a method on the WcfProductRepository class, it quickly opens a new channel and disposes of it after use. Its lifetime is extremely short, which is why we call such a disposable Abstraction an ephemeral disposable.
But wait! Didn’t we claim that a disposable Abstraction is a Leaky Abstraction? Yes, we did, but we have to balance pragmatic concerns against principles. In this case, at least, WcfProductRepository and IProductManagementService are defined in the same WCF-specific library. This ensures that the Leaky Abstraction can be confined to code that has a reasonable expectation of knowing about and managing that complexity.
Notice that the ephemeral disposable is never injected into the consumer. Instead, a factory is used, and you use that factory to control the lifetime of the ephemeral disposable.
ChannelFactory<TChannel> is thread-safe and can be injected as a Singleton. In this case, you might wonder why we choose to inject ChannelFactory<TChannel> into the WcfProductRepository’s constructor; you can create it internally and store it in a static field. This, however, causes WcfProductRepository to be implicitly dependent on a configuration file, which needs to exist to create a new WcfProductRepository. As we discussed in 2.2.3, only the finished application should rely on configuration files.
In summary, disposable Abstractions are Leaky Abstractions. Sometimes we must accept such a leak to avoid bugs (such as refused WCF connections); but when we do that, we should do our best to contain that leak so it doesn’t propagate throughout an entire application. We’ve now examined how to consume disposable Dependencies. Let’s turn our attention to how we can serve and manage them for consumers.
Because we so adamantly insist that disposable Abstractions are Leaky Abstractions, the consequence is that Abstractions shouldn’t be disposable. On the other hand, sometimes implementations are disposable; if you don’t properly dispose of them, you’ll have resource leaks in your applications. Someone or something must dispose of them.
As always, this responsibility falls on the Composer. It, better than anything else, knows when it creates a disposable instance, so it also knows when the instance needs to be disposed of. It’s easy for the Composer to keep a reference to the disposable instance and invoke its Dispose method at an appropriate time. The challenge lies in identifying when it’s the appropriate time. How do you know when all consumers have gone out of scope?
除非您在发生这种情况时被告知,否则您不知道。然而,您的代码通常存在于某种具有明确定义的生命周期的上下文中,以及告诉您特定范围何时完成的事件。例如,在 ASP.NET Core 中,您可以围绕单个 Web 请求确定实例范围。在 Web 请求结束时,框架会告诉IControllerActivator,通常是我们的Composer,它应该释放给定对象的所有依赖项。然后由Composer来跟踪这些Dependencies并决定是否必须根据他们的Lifestyles处理任何东西。
Unless you’re informed when that happens, you don’t know. Often, however, your code lives inside some sort of context with a well-defined lifetime, as well as events that tell you when a specific scope completes. In ASP.NET Core, for instance, you can scope instances around a single web request. At the end of a web request, the framework tells IControllerActivator, which is typically our Composer, that it should release all Dependencies for a given object. It’s then up to the Composer to keep track of those Dependencies and to decide whether anything must be disposed of based on their Lifestyles.
Releasing an object graph isn’t the same as disposing of it. As we stated in the introduction, releasing is the process of determining which Dependencies can be dereferenced and possibly disposed of, and which Dependencies should be kept alive to be reused. It’s the Composer that decides whether a released object should be disposed of or reused.
The release of an object graph is a signal to the Composer that the root of the graph is going out of scope, so if the root itself implements IDisposable, then it should be disposed of. But the root’s Dependencies can be shared with other roots, so the Composer may decide to keep some of them around, because it knows other objects still rely on them. Figure 8.5 illustrates the sequence of events.
To release Dependencies, a Composer must track all the disposable Dependencies it has ever served, and to which consumers it has served them, so that it can dispose of them when the last consumer is released. And a Composer must take care to dispose of objects in the correct order.
Let’s go back to the CommerceControllerActivator example from listing 8.3. As it turns out, there’s a bug in that listing, because CommerceContext implements IDisposable. The code in listing 8.3 creates new instances of CommerceContext, but it never disposes of those instances. This could cause resource leaks, so let’s fix that bug with a new version of the Composer.
首先,请记住 Web 应用程序的Composer必须能够为许多并发请求提供服务,因此它必须将每个CommerceContext实例与其创建的根对象或与其关联的请求相关联。在下面的示例中,我们将使用请求来跟踪一次性对象,因为这使我们不必定义静态字典实例。静态可变状态更难正确使用,因为它必须以线程安全的方式实现。下一个清单显示了如何解析实例CommerceControllerActivator请求。HomeController
First, keep in mind that the Composer for a web application must be able to service many concurrent requests, so it has to associate each CommerceContext instance with either the root object it creates or with the request it’s associated with. In the following example, we’ll use the request to track disposable objects, because this saves us from having to define a static dictionary instance. A static mutable state is more difficult to use correctly, because it must be implemented in a thread-safe manner. The next listing shows how CommerceControllerActivator resolves requests for HomeController instances.
Listing 8.7 Associating disposable Dependencies with a web request
private HomeController CreateHomeController(ControllerContext context)
{
var dbContext =
new CommerceContext(this.connectionString); ① TrackDisposable(context, dbContext); ②
return new HomeController(
new ProductService(
new SqlProductRepository(dbContext),
this.userContext));
}
private static void TrackDisposable(
ControllerContext context, IDisposable disposable)
{ ③ IDictionary<object, object> items = ③ context.HttpContext.Items; ③ ③ object list; ③ ③ if (!items.TryGetValue("Disposables", out list)) ③ { ③ list = new List<IDisposable>(); ③ items["Disposables"] = list; ③ } ③ ③ ((List<IDisposable>)list).Add(disposable); ③
}
The CreateHomeController method starts by resolving all the Dependencies. This is similar to the implementation in listing 8.3, but before returning the resolved service, it must store the Dependency with the request in such a way that it can be disposed of when the controller gets released. The application flow of listing 8.7 is shown in figure 8.6.
When we implemented the CommerceControllerActivator in listing 7.8, we left the Release method empty. So far, we haven’t implemented this method, relying on the garbage collector to do the job; but with disposable Dependencies, it’s essential that you take this opportunity to clean up. Here’s the implementation.
public void Release(ControllerContext context, object controller)
{
var disposables = ① (List<IDisposable>)context.HttpContext ① .Items["Disposables"]; ①
if (disposables != null)
{
disposables.Reverse(); ② foreach (IDisposable disposable in disposables) ③ { ③ disposable.Dispose(); ③
}
}
}
This Release method takes a shortcut that prevents some disposables from being disposed of if an exception is thrown. If you’re meticulous, you’ll need to ensure that disposal of instances continues, even if one throws an exception, preferably by using try and finally statements. We’ll leave this as an exercise for the reader.
在 ASP.NET Core MVC 的上下文中,使用和的给定解决方案可以简化为对 的简单调用,因为那样可以有效地做同样的事情。它既实现了相反顺序的处理,又在发生故障时继续处理对象。因为本章不是专门介绍 ASP.NET Core MVC,所以我们想为您提供一个更通用的解决方案来说明基本思想。TrackDisposableReleaseHttpContext.Response.RegisterForDispose
In the context of ASP.NET Core MVC, the given solution using TrackDisposable and Release can be reduced to a simple call to HttpContext.Response.RegisterForDispose, because that would effectively do the same thing. It both implements opposite-order disposal and continues disposing of objects in case of a failure. Because this chapter isn’t about ASP.NET Core MVC in particular, we wanted to provide you with a more generic solution that illustrates the basic idea.
After reading all this, two questions remain: where should object graphs be released, and who is responsible for doing this? It’s important to note that the code that has requested an object graph is also responsible for requesting its release. Because the request for an object graph is typically part of the Composition Root, so is the initiation of its release.
下面的清单Main再次显示了 7.1 节的控制台应用程序的方法,但现在多了一个Release方法。
The following listing shows the Main method of the console application of section 7.1 again, but now with an additional Release method.
When building a console application, you’re in full control of the application. As we discussed in section 7.1, there’s no Inversion of Control. If you’re using a framework, you’ll often see the framework take control over both requesting the object graph and demanding its release. ASP.NET Core MVC is a good example of this. In the case of MVC, it’s the framework that calls CommerceControllerActivator’s Create and Release methods. In between those calls, it uses a resolved controller instance.
We’ve now discussed Lifetime Management in some detail. As a consumer, you can’t manage the lifetime of injected Dependencies; that responsibility falls on the Composer who can decide to share a single instance among many consumers or give each consumer its own private instance. These Singleton and Transient Lifestyles are only the most common members of a larger set of Lifestyles, and we’ll use the next section to work our way through a catalog of the most common lifecycle strategies.
Now that we’ve covered the principles behind Lifetime Management, we’ll spend some time looking at common Lifestyle patterns. As we described in the introduction, a Lifestyle is a formalized way of describing the intended lifetime of a Dependency. This gives us a common vocabulary, just as design patterns do. It makes it easier to reason about when and how a Dependency is expected to go out of scope — and if it’ll be reused.
This section discusses the three most common Lifestyles described in table 8.1. Because you’ve already encountered both Singleton and Transient, we’ll begin with those.
The use of a Scoped Lifestyle is widespread; most exotic Lifestyles are variations of it. Compared to advanced Lifestyles, a Singleton Lifestyle may seem mundane, but it’s nevertheless a common and appropriate lifecycle strategy.
In this book, we’ve implicitly used the Singleton Lifestyle from time to time. The name is both clear and somewhat confusing at the same time. It makes sense, however, because the resulting behavior is similar to the Singleton design pattern, but the structure is different.
With both the Singleton Lifestyle and the Singleton design pattern, there’s only one instance of a Dependency, but the similarity ends there. The Singleton design pattern provides a global point of access to its instance, which is similar to the Ambient Context anti-pattern we discussed in section 5.3. A consumer, however, can’t access a Singleton-scoped Dependency through a static member. If you ask two different Composers to serve an instance, you’ll get two different instances. It’s important, therefore, that you don’t confuse the Singleton Lifestyle with the Singleton design pattern.
Because only a single instance is in use, the Singleton Lifestyle generally consumes a minimal amount of memory and is efficient. The only time this isn’t the case is when the instance is used rarely but consumes large amounts of memory. In such cases, the instance can be wrapped in a Virtual Proxy, as we’ll discuss in section 8.4.2.
何时使用单身生活方式
When to use the Singleton Lifestyle
尽可能使用单身生活方式。可能会阻止您使用Singleton的两个主要问题如下:
Use the Singleton Lifestyle whenever possible. Two main issues that might prevent you from using a Singleton follow:
When a component isn’t thread-safe. Because the Singleton instance is potentially shared among many consumers, it must be able to handle concurrent access.
When one of the component’s Dependencies has a lifetime that’s expected to be shorter, possibly because it isn’t thread-safe. Giving the component a Singleton Lifestyle would keep its Dependencies alive for too long. In that case, such a Dependency becomes a Captive Dependency. We’ll go into more detail about Captive Dependencies in section 8.4.1.
All stateless services are, by definition, thread-safe, as are immutable types and, obviously, classes specifically designed to be thread-safe. In these cases, there’s no reason not to configure them as Singletons.
In addition to the argument for efficiency, some Dependencies may work as intended only if they’re shared. For example, this is the case for implementations of the Circuit Breaker7 design pattern that we’ll discuss in chapter 9, as well as in-memory caches. In these cases, it’s essential that the implementations are thread-safe.
让我们仔细看看内存中的存储库。接下来我们将探讨一个例子。
Let’s take a closer look at an in-memory Repository. We’ll explore an example of this next.
示例:使用线程安全的内存存储库
Example: Using a thread-safe in-memory Repository
让我们再次将注意力转移到实现7.3.1 和 8.1.2 节中的类似内容。您可以使用线程安全的内存中实现,而不是使用基于 SQL Server 的。为了使内存数据存储有意义,它必须在所有请求之间共享,因此它必须是线程安全的。如图 8.7所示。CommerceControllerActivatorIProductRepository
Let’s once more turn our attention to implementing a CommerceControllerActivator like those from sections 7.3.1 and 8.1.2. Instead of using a SQL Server–based IProductRepository, you could use a thread-safe, in-memory implementation. For an in-memory data store to make sense, it must be shared among all requests, so it has to be thread-safe. This is illustrated in figure 8.7.
Figure 8.7 When multiple ProductService instances running on separate threads access a shared resource, such as an in-memory IProductRepository, you must ensure that the shared resource is thread-safe.
与其使用单例设计模式显式地实现这样的存储库,不如使用一个具体的类,并使用单例生活方式适当地限定它的范围。下一个清单显示了Composer如何在每次被要求解析 a 时返回新实例HomeController,而IProductRepository在所有实例之间共享。
Instead of explicitly implementing such a Repository using the Singleton design pattern, you should use a concrete class and scope it appropriately using the Singleton Lifestyle. The next listing shows how a Composer can return new instances every time it’s asked to resolve a HomeController, whereas IProductRepository is shared among all instances.
public class CommerceControllerActivator : IControllerActivator
{
private readonly IUserContext userContext; ① private readonly IProductRepository repository; ①
public CommerceControllerActivator()
{
this.userContext = new FakeUserContext(); ② this.repository = new InMemoryProductRepository(); ②
}
...
private HomeController CreateHomeController()
{
return new HomeController( ③ new ProductService( ③ this.repository, ③ this.userContext)); ③
}
}
Note that in this example, both repository and userContext encompass Singleton Lifestyles. You can, however, mix Lifestyles if you want. Figure 8.8 shows what happens with CommerceControllerActivator at runtime.
The Singleton Lifestyle is one of the easiest Lifestyles to implement. All it requires is that you keep a reference to the object and serve the same object every time it’s requested. The instance doesn’t go out of scope until the Composer goes out of scope. When that happens, the Composer should dispose of the object if it’s a disposable type.
另一种易于实现的生活方式是瞬态生活方式。让我们接下来看看。
Another Lifestyle that’s trivial to implement is the Transient Lifestyle. Let’s look at that next.
The Transient Lifestyle involves returning a new instance every time it’s requested. Unless the instance returned implements IDisposable, there’s nothing to keep track of. Conversely, when the instance implements IDisposable, the Composer must keep it in mind and explicitly dispose of it when asked to release the applicable object graph. Most of the examples in this book of constructed object graphs implicitly used the Transient Lifestyle.
It’s worth noting that in desktop and similar applications, we tend to resolve the entire object hierarchy only once: at application startup. This means that even for Transient components, only a few instances could be created, and they can be around for a long time. In the degenerate case where there’s only one consumer per Dependency, the end result of resolving a graph of pure Transient components is equivalent to resolving a graph of pure Singletons, or any mix thereof. This is because the graph is resolved only once, so the difference in behavior is never realized.
The Transient Lifestyle is the safest choice of Lifestyles, but also one of the least efficient. It can cause a myriad of instances to be created and garbage collected, even when a single instance would have sufficed.
If you have doubts about the thread-safety of a component, however, the Transient Lifestyle is safe, because each consumer has its own instance of the Dependency. In many cases, you can safely exchange the Transient Lifestyle for a Scoped Lifestyle, where access to the Dependency is also guaranteed to be sequential.
You saw several examples of using the Transient Lifestyle earlier in this chapter. In listing 8.3, the Repository is created and injected on the spot in the resolving method, and the Composer keeps no reference to it. In listings 8.8 and 8.9, you subsequently saw how to deal with a Transient disposable component.
In these examples, you may have noticed that the userContext stays a Singleton throughout. This is a purely stateless service, so there’s no reason to create a new instance for every ProductService created. The noteworthy point is that you can mix Dependencies with different Lifestyles.
当多个组件需要相同的Dependency时,每个组件都会被赋予一个单独的实例。以下清单显示了解析 ASP.NET Core MVC 控制器的方法。
When multiple components require the same Dependency, each is given a separate instance. The following listing shows a method resolving an ASP.NET Core MVC controller.
private HomeController CreateHomeController()
{
return new HomeController(
new ProductService(
new SqlProductRepository(this.connStr),
new AspNetUserContextAdapter(), ①
new SqlUserRepository(
this.connStr,
new AspNetUserContextAdapter()))); ①
}
The Transient Lifestyle implies that every consumer receives a private instance of the Dependency, even when multiple consumers in the same object graph have the same Dependency (as is the case in the previous listing). If many consumers share the same Dependency, this approach can be inefficient; but if the implementation isn’t thread-safe, the more efficient Singleton Lifestyle is inappropriate. In such cases, the Scoped Lifestyle may be a better fit.
8.3.3 有范围的生活方式
8.3.3 The Scoped Lifestyle
作为 Web 应用程序的用户,我们希望应用程序尽快做出响应,即使其他用户同时访问该系统也是如此。我们不希望我们的请求与所有其他用户的请求一起放入队列中。如果我们之前有很多请求,我们可能不得不等待过多的时间才能得到响应。为了解决这个问题,Web 应用程序并发处理请求。ASP.NET Core 基础结构通过让每个请求在其自己的上下文中并使用其自己的控制器实例(如果您使用 ASP.NET Core MVC)执行,从而使我们免受此影响。
As users of a web application, we’d like a response from the application as quickly as possible, even when other users are accessing the system at the same time. We don’t want our request to be put on a queue together with all the other users’ requests. We might have to wait an inordinate amount of time for a response if there are many requests ahead of ours. To address this issue, web applications handle requests concurrently. The ASP.NET Core infrastructure shields us from this by letting each request execute in its own context and with its own instance of controllers (if you use ASP.NET Core MVC).
Because of concurrency, Dependencies that aren’t thread-safe can’t be used as Singletons. On the other hand, using them as Transients can be inefficient or even downright problematic if you need to share a Dependency between different consumers within the same request.
尽管 ASP.NET Core 引擎异步执行单个请求,并且单个请求的执行通常涉及多个线程,但它确实保证代码以顺序方式执行 - 至少当您正确执行await异步操作时。8 这意味着如果您可以在单个请求中共享依赖项,线程安全就不是问题。第 8.4.3 节提供了有关异步、多线程方法如何在 ASP.NET Core 中工作的更多详细信息。
Although the ASP.NET Core engine executes a single request asynchronously, and the execution of a single request typically involves multiple threads, it does guarantee that code is executed in a sequential manner — at least when you properly await asynchronous operations.8 This means that if you can share a Dependency within a single request, thread-safety isn’t an issue. Section 8.4.3 provides more details on how the asynchronous, multi-threaded approach works in ASP.NET Core.
尽管 Web 请求的概念仅限于 Web 应用程序和 Web 服务,但请求的概念更为广泛。大多数长时间运行的应用程序使用请求来执行单个操作。例如,当构建一个一个一个地处理队列中的项目的服务应用程序时,您可以将每个处理的项目想象成一个单独的请求,由它自己的一组Dependencies组成。
Although the concept of a web request is limited to web applications and web services, the concept of a request is broader. Most long-running applications use requests to execute single operations. For example, when building a service application that processes items one by one from a queue, you can imagine each processed item as an individual request, consisting of its own set of Dependencies.
The same could hold for desktop or phone applications. Although the top root types (views or ViewModels) could potentially live for a long time, you could see a button press as a request, and you could scope this operation and give it its own isolated bubble with its own set of Dependencies. This leads to the concept of a Scoped Lifestyle, where you decide to reuse instances within a given scope. Figure 8.9 demonstrates how the Scoped Lifestyle works.
Note that DI Containers might have specialized versions of the Scoped Lifestyle that target a specific technology. Also, any disposable components should be disposed of when the scope ends.
何时使用Scoped Lifestyle
When to use the Scoped Lifestyle
Scoped Lifestyle对于长期运行的应用程序很有意义,这些应用程序的任务是处理需要以某种程度的隔离运行的操作。当并行处理这些操作时,或者当每个操作都包含自己的状态时,就需要隔离。Web 应用程序是Scoped Lifestyle运作良好的一个很好的例子,因为 Web 应用程序通常并行处理请求,而这些请求通常包含一些特定于请求的可变状态。但是,即使 Web 应用程序启动了一些与 Web 请求无关的后台操作,Scoped Lifestyle也是有价值的。甚至这些后台操作通常也可以映射到请求的概念。
The Scoped Lifestyle makes sense for long-running applications that are tasked with processing operations that need to run with some degree of isolation. Isolation is required when these operations are processed in parallel, or when each operation contains its own state. Web applications are a great example of where the Scoped Lifestyle works well, because web applications typically process requests in parallel, and those requests typically contain some mutable state that’s specific to the request. But even if a web application starts some background operation that isn’t related to a web request, the Scoped Lifestyle is valuable. Even these background operations can typically be mapped to the concept of a request.
As with all Lifestyles, you can mix the Scoped Lifestyle with others so that, for example, some Dependencies are configured as Singletons, and others are shared per request.
示例:使用作用域 DbContext 编写长时间运行的应用程序
Example: Composing a long-running application using a scoped DbContext
In this example, you’ll see how to compose a long-running console application with a scoped DbContextDependency. This console application is a variation of the UpdateCurrency program we discussed in section 7.1.
Just as with the UpdateCurrency program, this new console application reads currency exchange rates. The goal of this version, however, is to output the exchange rates of a particular currency amount once a minute and to continue to do so until the user stops the application. Figure 8.10 outlines the application’s main classes.
The CurrencyMonitoring program reuses the SqlExchangeRateProvider and CommerceContext from the UpdateCurrency program of chapter 7 and the ICurrencyConverterAbstraction from chapter 4. The ICurrencyRepositoryAbstraction and its accompanying SqlCurrencyRepository implementation are new. The CurrencyRateDisplayer is also new and is specific to this program; it’s shown in the following listing.
To piece the application together, you need to create the application’s Composition Root. The Composition Root, in this case, consists of two classes, as shown in figure 8.11.
The Program class uses the Composer class to resolve the application’s object graph. Listing 8.13 shows the Composer class with its CreateRateDisplayer method. It ensures that for each resolve, only one instance of the scoped CommerceContextDependency is created.
Listing 8.13 The Composer class, responsible for composing object graphs
public class Composer
{
private readonly string connectionString; ①
public Composer(string connectionString)
{
this.connectionString = connectionString;
}
public CurrencyRateDisplayer CreateRateDisplayer() ②
{
var context = ③ new CommerceContext(this.connectionString); ③
return new CurrencyRateDisplayer(
new SqlCurrencyRepository(
context), ④
new CurrencyConverter(
new SqlExchangeRateProvider(
context))); ④
}
}
The remaining part of the Composition Root is the application’s entry point: the Program class. It’s responsible for reading the input arguments and configuration file, and setting up the Timer that runs once a minute to display exchange rates. The following listing shows it in full glory.
Listing 8.14 The application’s entry point that manages scopes
public static class Program
{
private static Composer composer;
public static void Main(string[] args)
{
var money = new Money( ① currency: new Currency(code: args[0]), ① amount: decimal.Parse(args[1])); ①
composer = new Composer(LoadConnectionString());
var timer = new Timer(interval: 60000); ② timer.Elapsed += (s, e) => DisplayRates(money); ② timer.Start(); ②
Console.WriteLine("Press any key to exit.");
Console.ReadLine(); ③
}
private static void DisplayRates(Money money)
{
CurrencyRateDisplayer displayer =
composer.CreateRateDisplayer(); ④
displayer.DisplayRatesFor(money);
}
private static string LoadConnectionString() { ... }
}
The Program class configures a Timer that calls the DisplayRates method when it elapses. Even though you only call DisplayRates once per minute, in this example, you could easily call DisplayRates in parallel over multiple threads or even make DisplayRates asynchronous. This would still work because each call creates and manages its set of scoped instances, allowing each operation to run in isolation from the others.
Whereas a Transient Lifestyle implies that every consumer receives a private instance of a Dependency, a Scoped Lifestyle ensures that all consumers of all resolved graphs for that scope get the same instance. Besides common Lifestyle patterns, such as Singleton, Transient, and Scoped, there are also patterns that you can define as code smells or even anti-patterns. A few of those bad Lifestyle choices are discussed in the following section.
As we all know, some lifestyle choices are bad for our heath, smoking being one of them. The same holds true when it comes to applying Lifestyles in DI. You can make many mistakes. In this section, we discuss the choices shown in table 8.2.
As table 8.2 states, Captive Dependencies and the per-thread Lifestyle can cause bugs in your application. More often than not, these bugs only appear after deploying the application to production, because they are concurrency related. When we start the application, as developers, we typically run it for a short period of time, one request at a time. The same holds true for testers that typically go through the application in an orderly fashion. This might hide such problems, which only pop up when multiple users access the application concurrently.
When we leak details of our Lifestyle choices to our consumers, this typically won’t lead to bugs — or at least, not immediately. It does, however, complicate the Dependency’s consumers and their tests, and might cause sweeping changes throughout the code base. In the end, this increases the chance of bugs.
When it comes to lifetime management, a common pitfall is that of Captive Dependencies. This happens when a Dependency is kept alive by a consumer for longer than you intended it to be. This might even cause it to be reused by multiple threads or requests concurrently, even though the Dependency isn’t thread-safe.
An all-too-common example of a Captive Dependency is when a short-lived Dependency is injected into a Singleton consumer. A Singleton is kept alive for the lifetime of the Composer, and so will its Dependency. The following listing illustrates this problem.
public class Composer
{
private readonly IProductRepository repository;
public Composer(string connectionString)
{
this.repository = new SqlProductRepository( ① new CommerceContext(connectionString)); ②
}
...
}
Because there’s only one instance of SqlProductRepository for the entire application, and CommerceContext is referenced by SqlProductRepository in its private field, there will be effectively just one instance of CommerceContext too. This is a problem, because CommerceContext isn’t thread-safe and isn’t intended to outlive a single request. Because CommerceContext is kept captive by SqlProductRepository past its expected release time, we call CommerceContext a Captive Dependency.
使用DI Container时, Captive Dependencies是一个常见问题。这是由DI 容器的动态特性引起的,它很容易忘记您正在构建的对象图的形状。然而,正如前面的示例所示,使用Pure DI时也会出现问题。通过仔细构建Pure DI Composition Root中的代码,您可以减少遇到此问题的机会。下面的清单显示了这种方法的一个例子。
Captive Dependencies are a common problem when you’re working with a DI Container. This is caused by the dynamic nature of DI Containers that make it easy to lose track of the shape of the object graphs you’re building. As the previous example showed, however, the problem can also arise when working with Pure DI. By carefully structuring code in the Pure DI Composition Root, you can reduce the chance of running into this problem. The following listing shows an example of this approach.
Listing 8.16 Mitigating Captive Dependencies with Pure DI
public class CommerceControllerActivator : IControllerActivator
{
private readonly string connStr; ① private readonly IUserContext userContext; ①
public CommerceControllerActivator(string connectionString)
{
this.connStr = connectionString; ② this.userContext = ② new AspNetUserContextAdapter(); ②
}
public object Create(ControllerContext ctx)
{
var context = new CommerceContext(this.connStr); ③ var provider = new SqlExchangeRateProvider(context); ③ ③
Type type = ctx.ActionDescriptor
.ControllerTypeInfo.AsType();
if (type == typeof(HomeController))
{
return this.CreateHomeController(context); ④
}
else if (type == typeof(ExchangeController))
{
return this.CreateExchangeController(
context, provider); ④
}
else
{
throw new Exception("Unknown controller " + type.Name);
}
}
private HomeController CreateHomeController(
CommerceContext context)
{
return new HomeController( ⑤ new ProductService( ⑤ new SqlProductRepository( ⑤ context), ⑤ this.userContext)); ⑤
}
private RouteController CreateExchangeController(
CommerceContext context,
IExchangeRateProvider provider) { ... }
}
Listing 8.16 separates the creation of all Dependencies into three distinct phases. When you separate these phases, it becomes much easier to detect and prevent Captive Dependencies. These phases are
应用程序启动期间创建的单例
Singletons created during application start-up
在请求开始时创建的范围实例
Scoped instances created at the start of a request
根据请求,由Transient、Scoped和Singleton实例组成的特定对象图
Based on the request, a particular object graph that consists of Transient, Scoped, and Singleton instances
With this model, all the application’s Scoped Dependencies are created for each request, even when they aren’t used. This might seem inefficient, but remember that, as we discussed in section 4.2.2, component constructors should be free from all logic except guard checks and when storing incoming Dependencies. This makes construction fast and prevents most performance issues; the creation of a few unused Dependencies is a non-issue.
From a misconfiguration perspective, Captive Dependencies are one of the most common, hardest-to-spot configurations or programming errors related to bad Lifestyle choices. More often than we’d like to admit, we’ve wasted many hours trying to find bugs caused by Captive Dependencies. That’s why we consider tool support for spotting Captive Dependencies invaluable when you’re using a DI Container. Although Captive Dependencies are typically caused by configuration or programming errors, other inconvenient Lifestyle choices are design flaws, such as when you’re forcing Lifestyle choices on consumers.
8.4.2 使用Leaky Abstractions向消费者泄露生活方式选择
8.4.2 Using Leaky Abstractions to leak Lifestyle choices to consumers
Another case where you might end up with a bad Lifestyle choice is when you need to postpone the creation of a Dependency. When you have a Dependency that’s rarely needed and is costly to create, you might prefer to create such an instance on the fly, after the object graph is composed. This is a valid concern. What isn’t, however, is pushing such a concern on to the Dependency’s consumers. If you do this, you’re leaking details about the implementation and implementation choices of the Composition Root to the consumer. The Dependency becomes a Leaky Abstraction, and you’re violating the Dependency Inversion Principle.
In this section, we’ll show two common examples of how you can cause your Lifestyle choice to be leaked to a Dependency’s consumer. Both examples have the same solution: create a wrapper class that hides the Lifestyle choice and functions as an implementation of the original Abstraction rather than the Leaky Abstraction.
Let’s again return to our regularly reused ProductService example that was first introduced in listing 3.9. Let’s imagine that one of its Dependencies is costly to create, and not all code paths in the application require its existence.
This is something you might be tempted to solve by using .NET’s System.Lazy<T> class. A Lazy<T> allows access to an underlying value through its Value property. That value, however, will only be created when it’s requested for the first time. After that, the Lazy<T> caches the value for as long as the Lazy<T> instance exists.
This is useful, because it allows you to delay the creation of Dependencies. It’s an error, however, to inject Lazy<T> directly into a consumer’s constructor, as we’ll discuss later. The next listing shows an example of such an erroneous use of Lazy<T>.
Listing 8.18 Composing a ProductService that depends on Lazy<IUserContext>
Lazy<IUserContext> lazyUserContext =
new Lazy<IUserContext>( ①
() => new AspNetUserContextAdapter())
new HomeController(
new ProductService(
new SqlProductRepository(
new CommerceContext(connectionString)),
lazyUserContext)); ②
After seeing this code, you might wonder what’s so bad about it. The following discussion lists several problems with such a design, but it’s important that you know there’s nothing wrong with the use of Lazy<T> inside your Composition Root — injecting Lazy<T> into an application component, however, leads to Leaky Abstractions. Now, back to the problems.
First, letting a consumer depend on Lazy<IUserContext> complicates the consumer and its unit tests. You might think that having to call userContext.Value is a small price to pay for being able to lazy load an expensive Dependency, but it isn’t. When creating unit tests, not only do you have to create Lazy<T> instances that wrap the original Dependency, but you also have to write extra tests to verify whether that Value isn’t being called at the wrong time.
Because making the Dependency lazy seems important enough as a performance optimization, it would be weird not to verify whether you implemented it correctly. This is, at least, one extra test you need to write for every consumer of that Dependency. There might be dozens of consumers for such a Dependency, and they all need the extra tests to verify their correctness.
Second, changing an existing Dependency to a lazy Dependency later in the development process causes sweeping changes throughout the application. This can present a serious amount of effort when there are dozens of consumers for that Dependency, because, as discussed in the previous point, not only do the consumers themselves need to be altered, but all of their tests need to be changed too. Making these kinds of rippling changes is time consuming and risky.
To prevent this, you could make all Dependencies lazy by default, because, in theory, every Dependency could potentially become expensive in the future. This would prevent you from having to make any future cascading changes. But this would be madness, and we hope you agree that this isn’t a good path to pursue. This is especially true if you consider that every Dependency could potentially become a list of implementations, as we’ll discuss shortly. This would lead to making all DependenciesIEnumerable<Lazy<T>> by default, which would be, even more so, insane.
Last, because the amount of changes you have to make and the number of tests you need to add, it becomes quite easy to make programming mistakes that would completely nullify these changes. For instance, if you create a new component that accidentally depends on IUserContext instead of Lazy<IUserContext>, it means that every graph that contains that component will always get an eagerly loaded IUserContext implementation.
This doesn’t mean that you aren’t allowed to construct your Dependencies lazily, though. We’d like, however, to repeat our statement from section 4.2.1: you should keep the constructors of your components free of any logic other than Guard Clauses and the storing of incoming Dependencies. This makes the construction of your classes fast and reliable, and will prevent such components from ever becoming expensive to instantiate.
In some cases, however, you’ll have no choice; for instance, when dealing with third-party components you have little control over. In that case, Lazy<T> is a great tool. But rather than letting all consumers depend on Lazy<T>, you should hide Lazy<T> behind a Virtual Proxy and place that Virtual Proxy within the Composition Root.11 The following listing provides an example of this.
public class LazyUserContextProxy : IUserContext ①
{
private readonly Lazy<IUserContext> userContext; ②
public LazyUserContextProxy(
Lazy<IUserContext> userContext)
{
this.userContext = userContext;
}
public bool IsInRole(Role role)
{
IUserContext real = this.userContext.Value; ③ return real.IsInRole(role); ③
}
}
This new LazyUserContextProxy allows ProductService to dependent on IUserContext instead of Lazy<IUserContext>. Here’s ProductService’s new constructor:
public ProductService(
IProductRepository repository,
IUserContext userContext)
Listing 8.20 Composing a ProductService by injecting a Virtual Proxy
IUserContext lazyProxy =
new LazyUserContextProxy( ① new Lazy<IUserContext>( ① () => new AspNetUserContextAdapter())); ①
new HomeController(
new ProductService(
new SqlProductRepository(
new CommerceContext(connectionString)),
lazyProxy)); ②
As listing 8.19 shows, it’s not a bad thing per se to have a class depending on Lazy<T>, but you want to centralize this inside the Composition Root and only have a single class that takes this dependency on Lazy<IUserContext>. Depending on Func<T> has practically the same effect as depending on Lazy<T>, and the solution is similar. Doing so prevents your code from being complicated, unit tests from being added, sweeping changes from being made, and unfortunate bugs from being introduced. As you’ll see next, the same arguments hold for injecting IEnumerable<T> too.
Just as with using Lazy<T> to delay the creation of Dependencies, there are many cases where you need to work with a collection of Dependencies of a certain Abstraction. For this purpose, you can make use of one of the BCL collection Abstractions, such as IEnumerable<T>. Although, in itself, there’s nothing wrong with using IEnumerable<T> as an Abstraction to present a collection of Dependencies, using it in the wrong place can, once again, lead to a Leaky Abstraction. The following listing shows how IEnumerable<T> can be used incorrectly.
public class Component
{
private readonly IEnumerable<ILogger> loggers;
public Component(IEnumerable<ILogger> loggers) ①
{
this.loggers = loggers;
}
public void DoSomething()
{
foreach (var logger in this.loggers) ②
{
logger.Log("DoSomething called");
}
...
}
}
We’d like to prevent consumers from having to deal with the fact that there might be multiple instances of a certain Dependency. This is an implementation detail that’s leaking out through the IEnumerable<ILogger>Dependency. As we explained previously, every Dependency could potentially have multiple implementations, but your consumers shouldn’t need to be aware of this. Just as with the previous Lazy<T> example, this leakage increases the system’s complexity and maintenance costs when you have multiple consumers of such a Dependency, because every consumer has to deal with looping over the collection. So do consumer’s tests.
Although experienced developers spit out foreach constructs like this in a matter of seconds, things get more complicated when the collection of Dependencies needs to be processed differently. For example, let’s say that logging should continue even if one of the loggers fails:
foreach (var logger in this.loggers)
{
try
{
logger.Log("DoSomething called");
}
catch ① { ① } ①
}
Or, perhaps you not only want to continue processing, but also log that error to the next logger. This way, the next logger functions as a fallback for the failed logger:
for (int index = 0; index < this.loggers.Count; index++)
{
try
{
this.loggers[index].Log("DoSomething called"); ②
}
catch (Exception ex)
{
if (loggers.Count > index + 1)
{
loggers[index + 1].Log(ex); ③
}
}
}
Or perhaps — well, we think you get the idea. It’d be rather painful to have these kinds of code constructs all over the place. If you want to change your logging strategy, it causes you to make cascading changes throughout the application. Ideally, we’d like to centralize this knowledge to one single location.
You can fix this design problem using the Composite design pattern. You should be familiar with the Composite design pattern by now, as we’ve discussed it in chapters 1 and 6 (see figure 1.8, and listings 6.4 and 6.12). The next listing shows a Composite for ILogger.
public class CompositeLogger : ILogger ①
{
private readonly IList<ILogger> loggers; ②
public CompositeLogger(IList<ILogger> loggers)
{
this.loggers = loggers;
}
public void Log(LogEntry entry) ③
{
for (int index = 0; index < this.loggers.Count; index++)
{
try
{
this.loggers[index].Log(entry);
}
catch (Exception ex)
{
if (loggers.Count > index + 1)
{
var logger = loggers[index + 1];
logger.Log(new LogEntry(ex)); ④
}
}
}
}
}
The following snippet shows how you can compose the object graph for Component using this new CompositeLogger, keeping Component dependent on a single ILogger instead of an IEnumerable<ILogger>:
ILogger composite =
new CompositeLogger(new ILogger[] ①
{
new SqlLogger(connectionString),
new WindowsEventLogLogger(source: "MyApp"),
new FileLogger(directory: "c:\\logs")
});
new Component(composite); ②
As you’ve seen many times before, good application design follows the Dependency Inversion Principle and prevents Leaky Abstractions. This results in cleaner code that’s more maintainable and more resilient to programming errors. Let’s now look at a different smell, which doesn’t affect the application’s design per se, but potentially causes hard-to-fix concurrency problems.
8.4.3 将实例绑定到线程的生命周期导致并发错误
8.4.3 Causing concurrency bugs by tying instances to the lifetime of a thread
Sometimes you’re dealing with Dependencies that aren’t thread-safe but don’t necessarily need to be tied to the lifetime of a request. A tempting solution is to synchronizing the lifetime of such a Dependency to the lifetime of a thread. Although seductive, such practice is error prone.
Listing 8.23 shows how the CreateCurrencyParser method, previously discussed in listing 7.2, makes use of a SqlExchangeRateProviderDependency. This is created once for each thread in the application.
Listing 8.23 A Dependency’s lifetime tied to the lifetime of a thread
[ThreadStatic] ① private static CommerceContext context; ①
static CurrencyParser CreateCurrencyParser(
string connectionString)
{
if (context == null) ② { ② context = new CommerceContext( ② connectionString); ② } ② return new CurrencyParser( ③ new SqlExchangeRateProvider(context), ③ context); ③
}
尽管这看起来很无辜,但事实并非如此。接下来我们将讨论此清单的两个问题。
Although this might look innocent, that couldn’t be further from the truth. We’ll discuss two problems with this listing next.
It can be hard to predict what the lifespan of a thread is. When you create and start a thread using new Thread().Start(), you’ll get a fresh block of thread-static memory. This means that if you call CreateCurrencyParser in such a thread, the thread-static fields will all be unset, resulting in new instances being created.
When starting threads from the thread pool using ThreadPool.QueueUserWorkItem, however, you’ll possibly get an existing thread from the pool or a newly created thread, depending on what’s in the thread pool. Even if you aren’t creating threads yourself, the framework might be (as we’ve discussed regarding, for example, ASP.NET Core). This means that while some threads have a lifetime that’s rather short, others live for the duration of the entire application. Further complications arise when operations aren’t guaranteed to run on a single thread.
异步应用程序模型导致多线程问题
Asynchronous application models cause multi-threading issues
现代应用程序框架本质上是异步的。即使您的代码可能不使用asyncandawait关键字实现新的异步编程模式,您使用的框架可能仍会决定在与开始时不同的线程上完成请求。例如,ASP.NET Core 完全围绕这种异步编程模型构建。但更老ASP.NET Web API 和 ASP.NET Web Forms 等框架允许异步运行请求。
Modern application frameworks are inherently asynchronous in nature. Even though your code might not implement the new asynchronous programming patterns using the async and await keywords, the framework you’re using might still decide to finish a request on a different thread than it was started on. ASP.NET Core is, for instance, completely built around this asynchronous programming model. But even older frameworks, such as ASP.NET Web API and ASP.NET Web Forms, allow requests to run asynchronously.
This is a problem for Dependencies that are tied to a particular thread. When a request continues on a different thread, it still references the same Dependencies, even though some of them are tied to the original thread. Figure 8.12 illustrates this.
Using thread-specific Dependencies while running in an asynchronous context is a particularly bad idea, because it could lead to concurrency problems, which are typically hard to find and reproduce. Such a problem would only occur if the thread-specific Dependency isn’t thread-safe — they typically aren’t. Otherwise, the Singleton Lifestyle would have worked just fine.
The solution to this problem is to scope things around a request or operation, and there are several ways to achieve this. Instead of linking the lifetime of the Dependency to that of a thread, make its lifetime scoped to the request, as discussed in section 8.3.3. The following listing demonstrates this once more.
Listing 8.24 Storing Scoped Dependencies in local variables
static CurrencyParser CreateCurrencyParser(
string connectionString)
{
var context = new CommerceContext( ① connectionString); ① return new CurrencyParser( ② new SqlExchangeRateProvider(context), ② context); ②
}
The Lifestyles examined in this chapter represent the most common types, but you may have more exotic needs that aren’t satisfactorily addressed. When we find ourselves in such a situation, our immediate response should be to realize that our approach must be wrong, and if we change our design a bit, everything will fit nicely into standard patterns.
This realization is often a disappointment, but it leads to better and more maintainable code. The point is that if you feel the need to implement a custom Lifestyle or create a Leaky Abstraction, you should first seriously reconsider your design. For this reason, we decided to leave specialized Lifestyles out of this book. We can often handle such situations better with a redesign or Interception, as you’ll see in the next chapter.
The Composer has a greater degree of influence over the lifetime of Dependencies than any single consumer can have. The Composer decides when instances are created, and, by its choice of whether to share instances, it determines whether a Dependency goes out of scope with a single consumer or whether all consumers must go out of scope before the Dependency can be released.
Lifestyle是描述Dependency预期生命周期的形式化方式。
A Lifestyle is a formalized way of describing the intended lifetime of a Dependency.
The ability to fine tune each Dependency’s Lifestyle is important for performance reasons but can also be important for correct behavior. Some Dependencies must be shared between several consumers for the system to work correctly.
Liskov 替换原则指出,您必须能够在不改变系统正确性的情况下用抽象替换任意实现。
The Liskov Substitution Principle states that you must be able to substitute the Abstraction for an arbitrary implementation without changing the correctness of the system.
不遵守Liskov 替换原则会使应用程序变得脆弱,因为它不允许替换可能导致消费者中断的依赖项。
Failing to adhere to the Liskov Substitution Principle makes applications fragile, because it disallows replacing Dependencies that might cause a consumer to break.
短暂的一次性对象是具有明确且短生命周期的对象,通常不会超过单个方法调用。
An ephemeral disposable is an object with a clear and short lifetime that typically doesn’t exceed a single method call.
Diligently work to implement services so they don’t hold references to disposables, but rather create and dispose of them on demand. This makes memory management simpler, because the service can be garbage collected like other objects.
The responsibility of disposing of Dependencies falls to the Composer. It, better than anything else, knows when it creates a disposable instance, so it also knows that the instance needs to be disposed of.
Releasing is the process of determining which Dependencies can be dereferenced and (possibly) disposed of. The Composition Root signals the Composer to release a resolved Dependency.
A Composer must take care of the correct order of disposal for objects. An object might require its Dependencies to be called during disposal, which causes problems if these Dependencies are already disposed of. Disposal should, therefore, happen in the opposite order of object creation.
Within the scope of a single Composer, there’ll only be one instance of a component with the Singleton Lifestyle. Each time a consumer requests the component, the same instance is served.
Scoped Dependencies behave like singletons within a single, well-defined scope or request, but aren’t shared across scopes. Each scope has its own set of associated Dependencies.
The Scoped Lifestyle makes sense for long-running applications that are tasked with processing operations that need to run in some degree of isolation. Isolation is required when these operations are processed in parallel, or when each operation contains its own state.
如果您需要DbContext在 Web 请求中编写 Entity Framework Core,Scoped Lifestyle是一个很好的选择。DbContext实例不是线程安全的,但通常DbContext每个 Web 请求只需要一个实例。
If you ever need to compose an Entity Framework Core DbContext in a web request, a Scoped Lifestyle is an excellent choice. DbContext instances aren’t thread-safe, but you typically only want one DbContext instance per web request.
Object graphs can consist of Dependencies of different Lifestyles, but you should make sure that a consumer only has Dependencies with a lifetime that’s equal to or exceeds its own, because a consumer will keep its Dependencies alive. Failing to do so leads to Captive Dependencies.
A Captive Dependency is a Dependency that’s inadvertently kept alive for too long, because its consumer was given a lifetime that exceeds the Dependency’s expected lifetime.
使用DI 容器时,俘虏依赖项是错误的常见来源,尽管使用Pure DI时也会出现此问题。
Captive Dependencies are a common source of bugs when working with a DI Container, although the problem can also arise when working with Pure DI.
在应用Pure DI时,仔细构造Composition Root可以减少遇到问题的机会。
When applying Pure DI, a careful structure of the Composition Root can reduce the chance of running into problems.
When working with a DI Container, Captive Dependencies are such a widespread problem that some DI Containers perform analysis on constructed object graphs to detect them.
Sometimes you need to postpone the creation of a Dependency. Injecting the Dependency as a Lazy<T>, Func<T>, or IEnumerable<T>, however, is a bad idea because it causes the Dependency to become a Leaky Abstraction. Instead, you should hide this knowledge behind a Proxy or Composite.
Don’t bind the lifetime of a Dependency to the lifetime of a thread. The lifetime of a thread is often unclear, and using it in an asynchronous framework can cause multi-threading issues. Instead, use a proper Scoped Lifestyle or hide access to the thread-static value behind a Proxy.
9
拦截
9
Interception
在这一章当中
In this chapter
拦截两个协作对象之间的调用
Intercepting calls between two collaborating objects
One of the most interesting things about cooking is the way you can combine many ingredients, some of them not particularly savory in themselves, into a whole that’s greater than the sum of its parts. Often, you start with a simple ingredient that provides the basis for the meal, and then modify and embellish it until the end result is a delicious dish.
Consider a veal cutlet. If you were desperate, you could eat it raw, but in most cases you’d prefer to fry it. But if you slap it on a hot pan, the result will be less than stellar. Apart from the burned flavor, it won’t taste like much. Fortunately, there are lots of steps you can take to enhance the experience:
用黄油煎炸肉排可以防止肉烧焦,但味道可能会保持平淡。
Frying the cutlet in butter prevents burning the meat, but the taste is likely to remain bland.
加盐可以增强肉的味道。
Adding salt enhances the taste of the meat.
添加其他香料,例如胡椒粉,会使味道更加复杂。
Adding other spices, such as pepper, makes the taste more complex.
Breading it with a mixture that includes salt and spices not only adds to the taste, but also envelops the original ingredient in a new texture. At this point, you’re getting close to having a cotoletta.1
Slitting open a pocket in the cutlet and adding ham, cheese, and garlic into the pocket before breading it takes us over the top. Now you have veal cordon bleu, a most excellent dish.
The difference between a burned veal cutlet and veal cordon bleu is significant, but the basic ingredient is the same. The variation is caused by the things you add to it. Given a veal cutlet, you can embellish it without changing the main ingredient to create a different dish.
With loose coupling, you can perform a similar feat when developing software. When you program to an interface, you can transform or enhance a core implementation by wrapping it in other implementations of that interface. You already saw a bit of this technique in action in listing 8.19, where we used this technique to modify an expensive Dependency’s lifetime by wrapping it in a Virtual Proxy.2
这种方法可以推广,使您能够拦截消费者对服务的调用。这就是我们将在本章中介绍的内容。
This approach can be generalized, providing you with the ability to Intercept a call from a consumer to a service. This is what we’ll cover in this chapter.
Like the veal cutlet, we start out with a basic ingredient and add more ingredients to make the first ingredient better, but without changing the core of what it was originally. Interception is one of the most powerful abilities that you gain from loose coupling. It enables you to apply the Single Responsibility Principle and Separation of Concerns with ease.
In the previous chapters, we expended a lot of energy maneuvering code into a position where it’s truly loosely coupled. In this chapter, we’ll start harvesting the benefits of that investment. The overall structure of this chapter is pretty linear. We’ll start with an introduction to Interception, including an example. From there, we’ll move on to talk about Cross-Cutting Concerns. This chapter is light on theory and heavy on examples, so if you’re already familiar with this subject, you can consider moving directly to chapter 10, which discusses Aspect-Oriented Programming.
When you’re done with this chapter, you should be able to use Interception to develop loosely coupled code using the Decorator design pattern. You should gain the ability to successfully observe Separation of Concerns and apply Cross-Cutting Concerns, all while keeping your code in good condition.
This chapter starts with a basic, introductory example, building toward increasingly complex notions and examples. The final, and most advanced, concept can be quickly explained in the abstract. But, because it’ll probably only make sense with a solid example, the chapter culminates with a comprehensive, multipage demonstration of how it works. Before we get to that point, however, we must start at the beginning, which is to introduce Interception.
The concept of Interception is simple: we want to be able to intercept the call between a consumer and a service, and to execute some code before or after the service is invoked. And we want to do so in such a way that neither the consumer nor the service has to change.
For example, imagine you want to add security checks to a SqlProductRepository class. Although you could do this by changing SqlProductRepository itself or by changing a consumer’s code, with Interception, you apply security checks by intercepting calls to SqlProductRepository using some intermediary piece of code. In figure 9.1, a normal call from a consumer to a service is intercepted by an intermediary that can execute its own code before or after passing the call to the real service.
In this section, you’re going to get acquainted with Interception and learn how, at its core, it’s an application of the Decorator design pattern. Don’t worry if your knowledge of the Decorator pattern is a bit rusty; we’ll start with a description of this pattern as part of the discussion. When we’re done, you should have a good understanding of how Decorators work. We’ll begin by looking at a simple example that showcases the pattern, and follow up with a discussion of how Interception relates to the Decorator pattern.
9.1.1 装饰器设计模式
9.1.1 Decorator design pattern
与许多其他模式一样,装饰器模式是一种古老且描述良好的设计模式,它比 DI 早十年。它是Interception的一个基本部分,值得回顾一下。
As is the case with many other patterns, the Decorator pattern is an old and well-described design pattern that predates DI by a decade. It’s such a fundamental part of Interception that it warrants a refresher.
Erich Gamma 等人在设计模式:可重用面向对象软件的元素一书中首次描述了装饰器模式。(Addison-Wesley, 1994)。该模式的目的是“动态地将附加职责附加到对象。装饰器为扩展功能提供了一种灵活的子类化替代方案。” 3个
The Decorator pattern was first described in the book Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma et al. (Addison-Wesley, 1994). The pattern’s intent is to “attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.”3
As figure 9.2 shows, a Decorator works by wrapping one implementation of an Abstraction in another implementation of the same Abstraction. This wrapper delegates operations to the contained implementation, while adding behavior before and/or after invoking the wrapped object.
The ability to attach responsibilities dynamically means that you can make the decision to apply a Decorator at runtime rather than having this relationship baked into the program at compile time, which is what you’d do with subclassing.
A Decorator can wrap another Decorator, which wraps another Decorator, and so on, providing a “pipeline” of interception. Figure 9.3 shows how this works. At the core, there must be a self-contained implementation that performs the desired work.
Figure 9.3 Like a set of Russian nesting dolls, a Decorator wraps another Decorator that wraps a self-contained component.4
例如,假设您有一个名为Abstraction的方法,其中包含一个方法IGreeterGreet:
Let’s say, for instance, that you have an Abstraction called IGreeter that contains a Greet method:
public interface IGreeter
{
string Greet(string name);
}
对于这个抽象,您可以创建一个简单的实现来创建正式的问候语:
For this Abstraction, you can create a simple implementation that creates a formal greeting:
public class FormalGreeter : IGreeter
{
public string Greet(string name)
{
return "Hello, " + name + ".";
}
}
最简单的 Decorator 实现是在不执行任何操作的情况下委托对装饰对象的调用:
The simplest Decorator implementation is one that delegates the call to the decorated object without doing anything at all:
public class SimpleDecorator : IGreeter ①
{
private readonly IGreeter decoratee; ①
public SimpleDecorator(IGreeter decoratee)
{
this.decoratee = decoratee;
}
public string Greet(string name)
{
return this.decoratee.Greet(name); ②
}
}
Figure 9.4 shows the relationship between IGreeter, FormalGreeter, and SimpleDecorator. Because SimpleDecorator doesn’t do anything except forward the call, it’s pretty useless. Instead, a Decorator can choose to modify the input before delegating the call.
Figure 9.4 Both SimpleDecorator and FormalGreeter implement IGreeter, while SimpleDecorator wraps an IGreeter and forwards any calls from its Greet method to the Greet method of the decoratee.
我们先看Greet一个TitledGreeterDecorator类的方法:
Let’s take a look at the Greet method of a TitledGreeterDecorator class:
In a similar move, the Decorator may decide to modify the return value before returning it when you create a NiceToMeetYouGreeterDecorator:
public string Greet(string name)
{
string greet = this.decoratee.Greet(name);
return greet + " Nice to meet you.";
}
鉴于前面的两个示例,您可以将后者包裹在前者的周围以组成一个同时修改输入和输出的组合:
Given the two previous examples, you can wrap the latter around the former to compose a combination that modifies both input and output:
IGreeter greeter =
new NiceToMeetYouGreeterDecorator(
new TitledGreeterDecorator(
new FormalGreeter()));
string greet = greeter.Greet("Samuel L. Jackson");
Console.WriteLine(greet);
这会产生以下输出:
This produces the following output:
装饰器也可以决定不调用底层实现:
A Decorator may also decide not to invoke the underlying implementation:
public string Greet(string name)
{
if (name == null) ①
{
return "Hello world!";
}
return this.decoratee.Greet(name);
}
Not invoking the underlying implementation is more consequential than delegating the call. Although there’s nothing inherently wrong with skipping the decoratee, the Decorator now replaces, rather than enriches, the original behavior.5 A more common scenario is to stop execution by throwing an exception, as we’ll discuss in section 9.2.3.
What differentiates a Decorator from any class containing Dependencies is that the wrapped object implements the same Abstraction as the Decorator. This enables a Composer to replace the original component with a Decorator without changing the consumer. The wrapped object is often injected into the Decorator declared as the abstract type — it wraps the interface, not a specific, concrete implementation. In that case, the Decorator must adhere to the Liskov Substitution Principle and treat all decorated objects equally.
That’s it. There isn’t much more to the Decorator pattern than this. You’ve already seen Decorators in action several places in this book. The SecureMessageWriter example in section 1.2.2, for instance, is a Decorator. Now let’s look at a concrete example of how we can use a Decorator to implement a Cross-Cutting Concern.
9.1.2 示例:使用装饰器实现审计
9.1.2 Example: Implementing auditing using a Decorator
In this example, we’ll implement auditing for the IUserRepository again. As you might recall, we discussed auditing in section 6.3, where we used it as an example when explaining how to fix Dependency cycles. With auditing, you record all of the important actions users make in a system for later analysis.
Auditing is a common example of a Cross-Cutting Concern: it may be required, but the core functionality of reading and editing users shouldn’t be affected by auditing. This is exactly what we did in section 6.3. Because we injected the IAuditTrailAppender interface into the SqlUserRepository itself, we forced it to know about and to implement auditing. This is a Single Responsibility Principle violation. The Single Responsibility Principle suggests that we shouldn’t let SqlUserRepository implement auditing; given this, using a Decorator is a better alternative.
为用户存储库实现审计装饰器
Implementing an auditing Decorator for the user repository
You can implement auditing with a Decorator by introducing a new AuditingUserRepositoryDecorator class that wraps another IUserRepository and implements auditing. Figure 9.5 illustrates how the types relate to each other.
In addition to a decorated IUserRepository, AuditingUserRepositoryDecorator also needs a service that implements auditing. For this, you can use IAuditTrailAppender from section 6.3. The following listing shows this implementation.
AuditingUserRepositoryDecorator implements the same Abstraction that it decorates. It uses standard Constructor Injection to request an IUserRepository that it can wrap and to which it can delegate its core implementation. In addition to the decorated Repository, it also requests an IAuditTrailAppender it can use to audit the operations implemented by the decorated Repository. The following listing shows sample implementations of two methods on AuditingUserRepositoryDecorator.
public User GetById(Guid id) ① { ① return this.decoratee.GetById(id); ① } ① public void Update(User user) ② { ② this.decoratee.Update(user); ② this.appender.Append(user); ② } ②
Not all operations need auditing. A common requirement is to audit all create, update, and delete operations, while ignoring read operations. Because the GetById method is a pure read operation, you delegate the call to the decorated Repository and immediately return the result. The Update method, on the other hand, must be audited. You still delegate the implementation to the decorated Repository, but after the delegated method returns successfully, you use the injected IAuditTrailAppender to audit the operation.
A Decorator, like AuditingUserRepositoryDecorator, is similar to the breading around the veal cutlet: it embellishes the basic ingredient without modifying it. The breading itself isn’t an empty shell, but comes with its own list of ingredients. Real breading is made from breadcrumbs and spices; similarly, AuditingUserRepositoryDecorator contains an IAuditTrailAppender.
Note that the injected IAuditTrailAppender is itself an Abstraction, which means that you can vary the implementation independently of AuditingUserRepositoryDecorator. All the AuditingUserRepositoryDecorator class does is coordinate the actions of the decorated IUserRepository and IAuditTrailAppender. You can write any implementation of IAuditTrailAppender you like, but in listing 6.24, we chose to build one based on the Entity Framework. Let’s see how you can wire up all relevant Dependencies to make this work.
In chapter 8, you saw several examples of how to compose a HomeController instance. Listing 8.11 provided a simple implementation concerning instances with a Transient Lifestyle. The following listing shows how you can compose this HomeController using a decorated SqlUserRepository.
private HomeController CreateHomeController()
{
var context = new CommerceContext();
IAuditTrailAppender appender =
new SqlAuditTrailAppender(
this.userContext,
context);
IUserRepository userRepository = ① new AuditingUserRepositoryDecorator( ① appender, ① new SqlUserRepository(context)); ①
IProductService productService =
new ProductService(
new SqlProductRepository(context),
this.userContext,
userRepository); ②
return new HomeController(productService);
}
Notice that you were able to add behavior to IUserRepository without changing the source code of existing classes. You didn’t have to change SqlUserRepository to add auditing. Recall from section 4.4.2 that this is a desirable trait known as the Open/Closed Principle.
Now that you’ve seen an example of intercepting the concrete SqlUserRepository with a decorating AuditingUserRepositoryDecorator, let’s turn our attention to writing clean and maintainable code in the face of inconsistent or changing requirements, and to addressing Cross-Cutting Concerns.
Most applications must address aspects that don’t directly relate to any particular feature, but, rather, address a wider matter. These concerns tend to touch many otherwise unrelated areas of code, even in different modules or layers. Because they cut across a wide area of the code base, we call them Cross-Cutting Concerns. Table 9.1 lists some examples. This table isn’t a comprehensive listing; rather, it’s an illustrative sampling.
Figure 9.6 In application architecture diagrams, Cross-Cutting Concerns are typically represented by vertical blocks that span all layers. In this case, security is a Cross-Cutting Concern.
When you draw diagrams of layered application architecture, Cross-Cutting Concerns are often represented as vertical blocks placed beside the layers. This is shown in figure 9.6.
In this section, we’ll look at some examples that illustrate how to use Interception in the form of Decorators to address Cross-Cutting Concerns. From table 9.1, we’ll pick the fault tolerance, error handling, and security aspects to get a feel for implementing aspects. As is the case with many other concepts, Interception can be easy to understand in the abstract, but the devil is in the details. It takes exposure to properly absorb the technique, and that’s why this section shows three examples. When we’re done with these, you should have a clearer picture of what Interception is and how you can apply it. Because you already saw an introductory example in section 9.1.2, we’ll take a look at a more complex example to illustrate how Interception can be used with arbitrarily complex logic.
Any application that communicates with an out-of-process resource will occasionally find that the resource is unavailable. Network connections go down, databases go offline, and web services get swamped by Distributed Denial of Service (DDOS) attacks. In such cases, the calling application must be able to recover and appropriately deal with the issue.
大多数 .NET API 都有默认超时,以确保进程外调用不会永远阻塞使用线程。不过,在您收到超时异常的情况下,您如何处理对错误资源的下一次调用?您是否尝试再次调用该资源?因为超时通常表示另一端离线或被请求淹没,所以进行新的阻塞调用可能不是一个好主意。最好假设最坏的情况并立即抛出异常。这就是断路器模式背后的基本原理。
Most .NET APIs have default timeouts that ensure that an out-of-process call doesn’t block the consuming thread forever. Still, in a situation where you receive a timeout exception, how do you treat the next call to the faulting resource? Do you attempt to call the resource again? Because a timeout often indicates that the other end is either offline or swamped by requests, making a new blocking call may not be a good idea. It would be better to assume the worst and throw an exception immediately. This is the rationale behind the Circuit Breaker pattern.
Circuit Breaker is a stability pattern that adds robustness to an application by failing fast instead of hanging and consuming resources as it hangs. This is a good example of a non-functional requirement and a true Cross-Cutting Concern, because it has little to do with the feature implemented in the out-of-process call.
The Circuit Breaker pattern itself is a bit complex and can be intricate to implement, but you only need to make that investment once. You could even implement it in a reusable library if you liked, where you could easily apply it to multiple components by employing the Decorator pattern.
The Circuit Breaker design pattern takes its name from the electric switch of the same name.6 It’s designed to cut the connection when a fault occurs, preventing the fault from propagating.
In software applications, once a timeout or similar communications error occurs, it can make a bad situation worse if you keep hammering a downed system. If the remote system is swamped, multiple retries can take it over the edge — a pause might give it a chance to recover. On the calling tier, threads blocked waiting for timeouts can make the consuming application unresponsive, forcing a user to wait for an error message. It’s better to detect that communications are down and fail fast for a period of time.
The Circuit Breaker design addresses this by tripping the switch when an error occurs. It usually includes a timeout that makes it retry the connection later; this way, it can automatically recover when the remote system comes back up. Figure 9.7 illustrates a simplified view of the state transitions in a Circuit Breaker.
You may want to make a Circuit Breaker more complex than described in figure 9.7. First, you may not want to trip the breaker every time a sporadic error occurs but, rather, use a threshold. Second, you should only trip the breaker on certain types of errors. Timeouts and communication exceptions are fine, but a NullReferenceException is likely to indicate a bug instead of an intermittent error.
Let’s look at an example that shows how the Decorator pattern can be used to add Circuit Breaker behavior to an existing out-of-process component. In this example, we’ll focus on applying the reusable Circuit Breaker, but not on how it’s implemented.
示例:为创建断路器IProductRepository
Example: creating a Circuit Breaker for IProductRepository
IProductRepository在 7.2 节中,我们创建了一个 UWP 应用程序,它使用接口与后端数据源(例如 WCF 或 Web API 服务)进行通信. 在清单 8.6 中,我们使用了一个通过调用 WCF 服务操作来实现的。因为这个实现没有明确的错误处理,所以任何通信错误都会冒泡到调用者。WcfProductRepositoryIProductRepository
In section 7.2, we created a UWP application that communicates with a backend data source, such as a WCF or Web API service, using the IProductRepository interface. In listing 8.6, we used a WcfProductRepository that implements IProductRepository by invoking the WCF service operations. Because this implementation has no explicit error handling, any communication error will bubble up to the caller.
This is an excellent scenario in which to use a Circuit Breaker. You’d like to fail fast once exceptions start occurring; this way, you won’t block the calling thread and swamp the service. As the next listing shows, you start by declaring a Decorator for IProductRepository and requesting the necessary Dependencies via Constructor Injection.
public class CircuitBreakerProductRepositoryDecorator ① : IProductRepository ①
{
private readonly ICircuitBreaker breaker;
private readonly IProductRepository decoratee; ①
public CircuitBreakerProductRepositoryDecorator(
ICircuitBreaker breaker, ②
IProductRepository decoratee)
{
this.breaker = breaker;
this.decoratee = decoratee;
}
...
}
您现在可以包装对装饰的任何调用IProductRepository。
You can now wrap any call to the decorated IProductRepository.
Listing 9.5 Applying a Circuit Breaker to the Insert method
public void Insert(Product product)
{
this.breaker.Guard(); ①
try
{
this.decoratee.Insert(product); ②
this.breaker.Succeed();
}
catch (Exception ex) ③ { ③ this.breaker.Trip(ex); ③ throw; ③
}
}
在调用装饰存储库之前,您需要做的第一件事是检查断路器的状态。该Guard方法允许您在状态为 Closed 或 Half-Open 时通过,而在状态为 Open 时抛出异常。当您有理由相信调用不会成功时,这可以确保您快速失败。如果你通过了Guard方法,您可以尝试调用装饰的存储库。如果呼叫失败,您将触发断路器。在此示例中,我们保持简单,但在正确的实现中,您应该只捕获并从选定的异常类型中触发断路器。
The first thing you need to do before you invoke the decorated Repository is check the state of the Circuit Breaker. The Guard method lets you through when the state is either Closed or Half-Open, whereas it throws an exception when the state is Open. This ensures that you fail fast when you have reason to believe that the call isn’t going to succeed. If you make it past the Guard method, you can attempt to invoke the decorated Repository. If the call fails, you trip the breaker. In this example, we’re keeping things simple, but in a proper implementation, you should only catch and trip the breaker from a selection of exception types.
从 Closed 和 Half-Open 状态,使断路器跳闸会使您回到 Open 状态. 从打开状态,超时决定何时返回半打开状态.
From both the Closed and Half-Open states, tripping the breaker puts you back in the Open state. From the Open state, a timeout determines when you move back to the Half-Open state.
Conversely, you signal the Circuit Breaker if the call succeeds. If you’re already in the Closed state, you stay in the Closed state. If you’re in the Half-Open state, you transition back to Closed. It’s impossible to signal success when the Circuit Breaker is in the Open state, because the Guard method ensures that you never get that far.
All other methods of IProductRepository look similar, with the only difference being the method they invoke on the decoratee and an extra line of code for methods that return a value. You can see this variation inside the try block for the GetAll method:
var products = this.decoratee.GetAll();
this.breaker.Succeed();
return products;
Because you must indicate success to the Circuit Breaker, you have to hold the return value of the decorated repository before returning it. That’s the only difference between methods that return a value and methods that don’t.
此时,您已经离开了开放的实现,但真正的实现是一个完全可重用的复合类,它采用了 State 设计模式。7 虽然我们不打算深入探讨此处的实现,但重要的信息是您可以使用任意复杂的代码进行拦截。ICircuitBreakerCircuitBreaker
At this point, you’ve left the implementation of ICircuitBreaker open, but the real implementation is a completely reusable complex of classes that employ the State design pattern.7 Although we aren’t going to dive deeper into the implementation of CircuitBreaker here, the important message is that you can Intercept with arbitrarily complex code.
使用断路器实现组合应用程序
Composing the application using the Circuit Breaker implementation
To compose an IProductRepository with Circuit Breaker functionality added, you can wrap the Decorator around the real implementation:
var channelFactory = new ChannelFactory<IProductManagementService>("*");
var timeout = TimeSpan.FromMinutes(1);
ICircuitBreaker breaker = new CircuitBreaker(timeout);
IProductRepository repository =
new CircuitBreakerProductRepositoryDecorator( ①
breaker,
new WcfProductRepository(channelFactory));
In listing 7.6, we composed a UWP application from several Dependencies, including a WcfProductRepository instance in listing 8.6. You can decorate this WcfProductRepository by injecting it into a CircuitBreakerProductRepositoryDecorator instance, because it implements the same interface. In this example, you create a new instance of the CircuitBreaker class every time you resolve Dependencies. That corresponds to the Transient Lifestyle.
在 UWP 应用程序中,您只需解决一次依赖关系,使用瞬态断路器不是问题,但通常,这不是此类功能的最佳生活方式。另一端只有一个 Web 服务。如果此服务不可用,断路器应断开所有连接尝试。如果CircuitBreakerProductRepositoryDecorator使用了多个实例,则所有实例都应该发生这种情况。
In a UWP application, where you only resolve the Dependencies once, using a Transient Circuit Breaker isn’t an issue but, in general, this isn’t the optimal lifestyle for such functionality. There’ll only be a single web service at the other end. If this service becomes unavailable, the Circuit Breaker should disconnect all attempts to connect to it. If several instances of CircuitBreakerProductRepositoryDecorator are in use, this should happen for all of them.
There’s an obvious case for setting up CircuitBreaker with the Singleton lifetime, but that also means that it must be thread-safe. Due to its nature, CircuitBreaker maintains state; thread-safety must be explicitly implemented. This makes the implementation even more complex.
Despite its complexity, you can easily Intercept an IProductRepository instance with a Circuit Breaker. Although the first Interception example in section 9.1.2 was fairly simple, the Circuit Breaker example demonstrates that you can intercept a class with a Cross-Cutting Concern. The Cross-Cutting Concern can easily be more complex than the original implementation.
The Circuit Breaker pattern ensures that an application fails fast instead of tying up precious resources. Ideally, the application wouldn’t crash at all. To address this issue, you can implement some kinds of error handling with Interception.
9.2.2 使用装饰器模式报告异常
9.2.2 Reporting exceptions using the Decorator pattern
依赖项可能会不时抛出异常。如果遇到无法处理的情况,即使是写得最好的代码也会(并且应该)抛出异常。消耗进程外资源的客户端属于这一类。示例 UWP 应用程序中的类就是一个示例。当 Web 服务不可用时,存储库将开始抛出异常。断路器不会改变这一基本特征。虽然它拦截了 WCF 客户端,但它仍然会抛出异常——它做得更快。WcfProductRepository
Dependencies are likely to throw exceptions from time to time. Even the best-written code will (and should) throw exceptions if it encounters situations it can’t deal with. Clients that consume out-of-process resources fall into that category. A class like WcfProductRepository from the sample UWP application is one example. When the web service is unavailable, the Repository will start throwing exceptions. A Circuit Breaker doesn’t change this fundamental trait. Although it Intercepts the WCF client, it still throws exceptions — it does so quicker.
You can use Interception to add error handling. You don’t want to burden a Dependency with error handling. Because a Dependency should be viewed as a reusable component that can be consumed in many different scenarios, it wouldn’t be possible to add an exception-handling strategy to the Dependency that would fit all scenarios. It would also be a violation of the Single Responsibility Principle if you did.
Figure 9.8 The product-management application handles communication exceptions by showing a message to the user. Notice that in this case, the error message originates from the Circuit Breaker instead of the underlying communication failure.
By using Interception to deal with exceptions, you follow the Open/Closed Principle. It allows you to implement the best error-handling strategy for any given situation. Let’s look at an example.
In the previous example, we wrapped WcfProductRepository in a Circuit Breaker for use with the product-management client application, which was originally introduced in section 7.2.2. A Circuit Breaker only deals with errors by making certain that the client fails fast, but it still throws exceptions. If left unhandled, they’ll cause the application to crash, so you should implement a Decorator that knows how to handle some of those errors.
Instead of a crashing application, you might prefer a message box that tells the user that the operation didn’t succeed and that they should try again later. In this example, when an exception is thrown, it should pop up a message as shown in figure 9.8.
Implementing this behavior is easy. The same way you did in section 9.2.1, you add a new ErrorHandlingProductRepositoryDecorator class that decorates the IProductRepository interface. Listing 9.6 shows a sample of one of the methods of that interface, but they’re all similar.
The Insert method is representative of the entire implementation of the ErrorHandlingProductRepositoryDecorator class. You attempt to invoke the decoratee and alert the user with the error message if an exception is thrown. Notice that you only handle a particular set of known exceptions, because it can be dangerous to suppress all exceptions. Alerting the user involves formatting a string and showing it to the user using the MessageBox.Show method. This is done inside the AlertUser method.
Once again, you added functionality to the original implementation (WcfProductRepository) by implementing the Decorator pattern. You’re following both the Single Responsibility Principle and the Open/Closed Principle by continually adding new types instead of modifying existing code. By now, you should be seeing a pattern that suggests a more general arrangement than a Decorator. Let’s briefly glance at a final example, implementing security.
9.2.3 使用装饰器防止未经授权访问敏感功能
9.2.3 Preventing unauthorized access to sensitive functionality using a Decorator
安全是另一个常见的交叉问题。我们希望尽可能保护我们的应用程序,以防止未经授权访问敏感数据和功能。
Security is another common Cross-Cutting Concern. We want to secure our applications as much as possible to prevent unauthorized access to sensitive data and functionality.
Similar to how we used Circuit Breaker, we’d like to Intercept a method call and check whether the call should be allowed. If not, instead of allowing the call to be made, an exception should be thrown. The principle is the same; the difference lies in the criterion we use to determine the validity of the call.
实现授权逻辑的一种常见方法是通过检查用户角色与手头操作的硬编码值来采用基于角色的安全性。如果我们坚持我们的IProductRepository,我们可能会从 a 开始。因为,正如您在前面几节中看到的,所有方法看起来都很相似,所以下面的清单只显示了两个方法实现。SecureProductRepositoryDecorator
A common approach to implementing authorization logic is to employ role-based security by checking the user’s role(s) against a hard-coded value for the operation at hand. If we stick with our IProductRepository, we might start out with a SecureProductRepositoryDecorator. Because, as you’ve seen in the previous sections, all methods look similar, the following listing only shows two method implementations.
In our current design, for a given Cross-Cutting Concern, the implementation based on a Decorator tends to be repetitive. Implementing a Circuit Breaker involves applying the same code template to all methods of the IProductRepository interface. Had you wanted to add a Circuit Breaker to another Abstraction, you would’ve had to apply the same code to more methods.
With the security Decorator, it got even worse because we required some of the methods to be extended, whereas others are mere pass-through operations. But the overall problem is identical.
If you need to apply this Cross-Cutting Concern to a different Abstraction, this too will cause code duplication, which can cause major maintainability issues as the system gets bigger. As you might imagine, there are ways to prevent code duplication, bringing us to the important topic of Aspect-Oriented Programming, which we’ll discuss in the next chapter.
Interception is the ability to intercept calls between two collaborating components in such a way that you can enrich or change the behavior of the Dependency without the need to change the two collaborators themselves.
Loose coupling is the enabler of Interception. When you program to an interface, you can transform or enhance a core implementation by wrapping it in other implementations of that interface.
Interception的核心是 Decorator 设计模式的应用。
At its core, Interception is an application of the Decorator design pattern.
The Decorator design pattern provides a flexible alternative to subclassing by attaching additional responsibilities to an object dynamically. It works by wrapping one implementation of an Abstraction in another implementation of the same Abstraction. This allows Decorators to be nested like Russian nesting dolls.
Cross-Cutting Concerns are non-functional aspects of code that typically cut across a wide area of the code base. Common examples of Cross-Cutting Concerns are auditing, logging, validation, security, and caching.
断路器是一种稳定性设计模式,它通过在发生故障时切断连接来增加系统的鲁棒性,以防止故障传播。
Circuit Breaker is a stability design pattern that adds robustness to a system by cutting connections when a fault occurs in order to prevent the fault from propagating.
10
设计的面向方面编程
10
Aspect-Oriented Programming by design
在这一章当中
In this chapter
重述SOLID原则
Recapping the SOLID principles
使用面向方面的编程来防止代码重复
Using Aspect-Oriented Programming to prevent code duplication
使用SOLID实现面向切面编程
Using SOLID to achieve Aspect-Oriented Programming
在家做饭和在专业厨房工作有很大区别。在家里,您可以随心所欲地准备菜肴,但在商用厨房中,效率是关键。Mise en place是其中的一个重要方面。这不仅仅是提前准备配料;它是关于设置所有必需的设备,包括您的锅、平底锅、砧板、品尝勺,以及任何您工作空间必不可少的东西。
There’s a big difference between cooking at home and working in a professional kitchen. At home, you can take all the time you want to prepare your dish, but in a commercial kitchen, efficiency is key. Mise en place is an important aspect of this. This is more than in-advance preparation of ingredients; it’s about having all the required equipment set up, including your pots, pans, chopping boards, tasting spoons, and anything that’s an essential part of your workspace.
The ergonomics and layout of the kitchen is also a major factor in the efficiency of a kitchen. A badly laid out kitchen can cause pinch points, high levels of disruption, and context switching for staff. Features like dedicated stations with associated specialized equipment help to minimize the movement of staff, avoid (unnecessary) multitasking, and encourage concentration on the task at hand. When this is done well, it helps to improve the efficiency of the kitchen as a whole.
In software development, the code base is our kitchen. Teams work together for years in the same kitchen, and the right architecture is essential to be efficient and consistent, keeping code repetition to a minimum. Your “guests” depend on your successful kitchen strategy.
One of the key architectural strategies you can use to improve your software ergonomics is Aspect-Oriented Programming (AOP). This can come in the form of equipment (tools) or a solid layout (software design). AOP is strongly related to Interception. To fully appreciate the potential of Interception, you must study the concept of AOP and software design principles like SOLID.
This chapter starts with an introduction to AOP. Because one of the most effective ways to apply AOP is through well-known design patterns and object-oriented principles, this chapter continues with a recap of the five SOLID principles, which were discussed in previous chapters throughout the book.
A common misconception is that AOP requires tooling. In this chapter, we’ll demonstrate that this isn’t the case: We’ll show how you can use SOLID software design as a driver of AOP and an enabler of an efficient, consistent, and maintainable code base. In the next chapter, we’ll discuss two well-known forms of AOP that require special tooling. Both forms, however, exhibit considerable disadvantages over the purely design-driven form of AOP discussed in this chapter.
If you’re already familiar with SOLID and the basics of AOP, you can jump directly into section 10.3, which contains the meat of this chapter. Otherwise, you can continue with our introduction to Aspect-Oriented Programming.
AOP was invented at the Xerox Palo Alto Research Center (PARC) in 1997, where Xerox engineers designed AspectJ, an AOP extension to the Java language. AOP is a paradigm that focuses around the notion of applying Cross-Cutting Concerns effectively and maintainably. It’s a fairly abstract concept that comes with its own set of jargon, most of which isn’t pertinent to this discussion.
The auditing and Circuit Breaker examples in sections 9.1.2 and 9.2.1 showed only a few representative methods, because all methods were implemented in the same way. We didn’t want to add several pages of nearly identical code to our discussion because it would’ve detracted from the point we were making.
Listing 10.2 shows how similar the methods of CircuitBreakerProductRepositoryDecorator are. This listing only shows the Insert method, but we’re confident that you can extrapolate how the rest of the implementation would look.
The purpose of this listing is to illustrate the repetitive nature of Decorators used as aspects in our current design. The only difference between the Delete and Insert methods is that they each invoke their own corresponding method on the decorated Repository.
Even though we’ve successfully delegated the Circuit Breaker implementation to a separate class via the ICircuitBreaker interface, this plumbing code violates the DRY principle. It tends to be reasonably unchanging, but it’s still a liability. Every time you want to add a new member to a type you decorate, or when you want to apply a Circuit Breaker to a new Abstraction, you must apply the same plumbing code. This repetitiveness can become a problem if you want to maintain such an application.
Sticking with our auditing example from chapter 9, we’ve already established that you don’t want to put the auditing code inside the SqlProductRepository implementation, because that would violate the Single Responsibility Principle (SRP). But neither do you want to have dozens of auditing Decorators for each Repository Abstraction in the system. This would also cause severe code duplication and, likely, sweeping changes, which is an Open/Closed Principle (OCP) violation. Instead, you want to declaratively state that you want to apply the auditing aspect to a certain set of methods of all Repository Abstractions in the system and implement this auditing aspect once.
You’ll find tools, frameworks, and architectural styles that enable AOP. In this chapter, we’ll discuss the most ideal form of AOP. The next chapter will discuss dynamic Interception and compile-time weaving as tool-based forms of AOP. These are the three major methods of AOP.1Table 10.1 lists the methods we’ll discuss, with a few of the major advantages and disadvantages of each.
As stated previously, we’ll get back to dynamic Interception and compile-time weaving in the next chapter. But before we dive into using SOLID as a driver for AOP, let’s start with a short recap of the SOLID principles.
You may have noticed a denser-than-usual usage of terms such as Single Responsibility Principle, Open/Closed Principle, and Liskov Substitution Principle in chapter 9 and in the previous section. Together with the Interface Segregation Principle (ISP) and Dependency Inversion Principle (DIP), they make up the SOLID acronym. We’ve discussed all five of them independently throughout the course of this book, but this section provides a short summary to refresh your mind, because understanding those principles is important for the remainder of this chapter.
所有这些模式和原则都被认为是编写干净代码的宝贵指南。本节的一般目的是将这一既定指南与 DI 联系起来,强调 DI 只是达到目的的一种手段。因此,我们使用 DI 作为可维护代码的推动者。
All these patterns and principles are recognized as valuable guidance for writing clean code. The general purpose of this section is to relate this established guidance to DI, emphasizing that DI is only a means to an end. We, therefore, use DI as an enabler of maintainable code.
None of the principles encapsulated by SOLID represent absolutes. They’re guidelines that can help you write clean code. To us, they represent goals that help us decide which direction we should take our applications. We’re always happy when we succeed; but sometimes we don’t.
The following sections go through the SOLID principles and summarize what we’ve already explained about them throughout the course of this book. Each section is a brief overview — we omit examples in those sections. We’ll return to this in section 10.3, where we walk through a realistic example that shows why a violation of the SOLID principles can become problematic from a maintainability perspective. For now, we’ll recap the five SOLID principles.
In section 2.1.3, we described how the SRP states that every class should have a single reason to change. Violating this principle causes classes to become more complex and harder to test and maintain.
More often than not, however, it can be challenging to see whether a class has multiple reasons to change. What can help in this respect is looking at the SRP from the perspective of cohesion. Cohesion is defined as the functional relatedness of the elements of a class or module. The lower the amount of relatedness, the lower the cohesion; and the lower the cohesion, the greater the possibility a class violates the SRP. In section 10.3, we’ll discuss cohesion with a concrete example.
It can be difficult to stick to, but if you practice DI, one of the many benefits of Constructor Injection is that it becomes more obvious when you violate the SRP. In the auditing example in section 9.1.2, you were able to adhere to the SRP by separating responsibilities into separate types: SqlUserRepository deals only with storing and retrieving product data, whereas AuditingUserRepositoryDecorator concentrates on persisting the audit trail in the database. The AuditingUserRepositoryDecorator class’s single responsibility is to coordinate the actions of IUserRepository and IAuditTrailAppender.
As we discussed in section 4.4.2, the OCP prescribes an application design that prevents you from having to make sweeping changes throughout the code base; or, in the vocabulary of the OCP, a class should be open for extension, but closed for modification. A developer should be able to extend the functionality of a system without needing to modify the source code of any existing classes.
Because they both try to prevent sweeping changes, there’s a strong relationship between the OCP principle and the Don’t Repeat Yourself (DRY) principle. OCP, however, focuses on code, whereas DRY focuses on knowledge.
You can make a class extensible in many ways, including virtual methods, injection of Strategies, and the application of Decorators.3 But no matter the details, DI makes this possible by enabling you to compose objects.
In section 8.1.1, we described that all consumers of Dependencies should observe the LSP when they invoke their Dependencies ,because every Dependency should behave as defined by its Abstraction. This allows you to replace the originally intended implementation with another implementation of the same Abstraction, without worrying about breaking a consumer. Because a Decorator implements the same Abstraction as the class it wraps, you can replace the original with a Decorator, but only if that Decorator adheres to the contract given by its Abstraction.
This was exactly what we did in listing 9.3 when we substituted the original SqlUserRepository with AuditingUserRepositoryDecorator. You could do this without changing the code of the consuming ProductService, because any implementation should adhere to the LSP. ProductService requires an instance of IUserRepository and, as long as it talks exclusively to that interface, any implementation will do.
LSP 是 DI 的基础。当消费者不观察它时,注入Dependencies几乎没有优势,因为您不能随意替换它们,并且您将失去 DI 的许多(如果不是全部)好处。
The LSP is a foundation of DI. When consumers don’t observe it, there’s little advantage in injecting Dependencies, because you can’t replace them at will, and you’ll lose many (if not all) benefits of DI.
In section 6.2.1, you learned that the ISP promotes the use of fine-grained Abstractions, rather than wide Abstractions. Any time a consumer depends on an Abstraction where some of its members are unused, the ISP is violated.
乍一看,ISP 似乎与 DI 关系很远,但这可能是因为我们在本书的大部分内容中都忽略了这一原则。这将在 10.3 节中改变,您将在该节中了解到 ISP 在有效应用面向方面编程时至关重要。
The ISP can, at first, seem to be distantly related to DI, but that’s probably because we ignored this principle for most of this book. That’ll change in section 10.3, where you’ll learn that the ISP is crucial when it comes to effectively applying Aspect-Oriented Programming.
10.2.5 依赖倒置原则(DIP)
10.2.5 Dependency Inversion Principle (DIP)
当我们在 3.1.2 节中讨论 DIP 时,您了解到我们试图用 DI 完成的大部分工作都与 DIP 相关。该原则指出您应该针对Abstractions进行编程,并且消费层应该控制消费Abstraction的形状。消费者应该能够以最有利于自己的方式定义抽象。如果您发现自己向接口添加成员以满足其他特定实现(包括未来可能的实现)的需要,那么您几乎肯定违反了 DIP。
When we discussed the DIP in section 3.1.2, you learned that much of what we’re trying to accomplish with DI is related to the DIP. The principle states that you should program against Abstractions, and that the consuming layer should be in control of the shape of a consumed Abstraction. The consumer should be able to define the Abstraction in a way that benefits itself the most. If you find yourself adding members to an interface to satisfy the needs of other, specific implementations — including potential future implementations — then you’re almost certainly violating the DIP.
10.2.6 SOLID原则和拦截
10.2.6 SOLID principles and Interception
设计模式(如 Decorator)和指南(如SOLID原则)已经存在多年,通常被认为是有益的。在这些部分中,我们提供了它们与 DI 的关系的说明。
Design patterns (such as Decorator) and guidelines (such as SOLID principles) have been around for many years and are generally regarded as beneficial. In these sections, we provide an indication of how they relate to DI.
The SOLID principles have been relevant throughout the book’s chapters. But it’s when we start talking about Interception and how it relates to Decorators that the benefits of adhering to the SOLID principles stands out. Some are subtler than others, but adding behavior (such as auditing) by using a Decorator is a clear application of both the OCP and the SRP, the latter allowing us to create implementations with specifically defined scopes.
在前面的部分中,我们简要介绍了常见的模式和原则,以了解 DI 与其他既定指南的关系。有了这些知识,现在让我们将注意力转回本章的目标,即在面对不一致或不断变化的需求时编写干净且可维护的代码,以及解决横切关注点的需要。
In the previous sections, we took a short detour through common patterns and principles to understand the relationship DI has with other established guidelines. Armed with this knowledge, let’s now turn our attention back to the goal of the chapter, which is to write clean and maintainable code in the face of inconsistent or changing requirements, as well as the need to address Cross-Cutting Concerns.
In section 10.1, you learned that the primary aim of AOP is to keep your Cross-Cutting Concerns DRY. As we discussed in section 10.2, there’s a strong relationship between the OCP and the DRY principle. They both strive for the same objective, which is to minimize repetition and prevent sweeping changes.
From that perspective, the code repetition that you witnessed with AuditingUserRepositoryDecorator, CircuitBreakerProductRepositoryDecorator, and SecureProductRepositoryDecorator in chapter 9 (listings 9.2, 9.4, and 9.7) are a strong indication that we were violating the OCP. AOP seeks to address this by separating out extensible behavior (aspects) into separate components that can easily be applied to a variety of implementations.
A common misconception, however, is that AOP requires tooling. AOP tool vendors are all to eager to keep this fallacy alive. Our preferred approach is to practice AOP by design, which means you apply patterns and principles first, before reverting to specialized AOP tooling like dynamic Interception libraries.
In this section, we’ll do just that. We’ll look at AOP from a design perspective by taking a close look at the IProductServiceAbstraction we introduced in chapter 3. We’ll analyze which SOLID principles we’re violating and why such violations are problematic. After that, we’ll address these violations step by step with the goal of making the application more maintainable, preventing the need to make sweeping changes in the future. Be prepared for some mental discomfort — and even cognitive dissonance — as we defy your beliefs on how to design software. Buckle up, and get ready for the ride.
10.3.1 示例:使用 IProductService 实现与产品相关的功能
10.3.1 Example: Implementing product-related features using IProductService
Let’s dive right in by looking at the IProductServiceAbstraction that you built in chapter 3 as part of the sample e-commerce application’s domain layer. The following listing shows this interface as originally defined in listing 3.5.
When looking at an application’s design from the perspective of SOLID principles in general, and the OCP in particular, it’s important to take into consideration how the application has changed over time, and from there predict future changes. With this in mind, you can determine whether the application is closed for modification to the changes that are most likely to happen in the future.
It’s important to note that even with a SOLID design, there can come a time where a change becomes sweeping. Being 100% closed for modification is neither possible nor desirable. Besides, conforming to the OCP is expensive. It takes considerable effort to find and design the appropriate Abstractions, although too many Abstractions can have a negative impact on the complexity of the application. Your job is to balance the risks and the costs and come up with a global optimum.
因为您应该查看应用程序的演变方式,所以IProductService在单个时间点进行评估并没有多大帮助。幸运的是,Mary Rowan(我们第 2 章的开发人员)已经在她的电子商务应用程序上工作了一段时间,并且自从我们上次查看她的肩上以来已经实现了许多功能。下一个清单显示了 Mary 的进步情况。
Because you should be looking at how the application evolves, evaluating IProductService at a single point in time isn’t that helpful. Fortunately, Mary Rowan (our developer from chapter 2), has been working on her e-commerce application for some time now, and a number of features have been implemented since we last looked over her shoulder. The next listing shows how Mary has progressed.
Listing 10.4 The evolved IProductService interface
public interface IProductService
{
IEnumerable<DiscountedProduct> GetFeaturedProducts();
void DeleteProduct(Guid productId); ① Product GetProductById(Guid productId); ① void InsertProduct(Product product); ① void UpdateProduct(Product product); ① Paged<Product> SearchProducts( ① int pageIndex, int pageSize, ① Guid? manufacturerId, string searchText); ① void UpdateProductReviewTotals( ① Guid productId, ProductReview[] reviews); ① void AdjustInventory( ① Guid productId, bool decrease, int quantity); ① void UpdateHasTierPricesProperty(Product product); ① void UpdateHasDiscountsApplied( ① Guid productId, string discountDescription); ①
}
As you can see, quite a few new features have been added to the application. Some are typical CRUD operations, such as UpdateProduct, whereas others address more-complex use cases, such as UpdateHasTierPricesProperty. Still others are for retrieving data, such as SearchProducts and GetProductById.
Although Mary started off with good intentions when she defined the first version of IProductService in listing 10.3, the fact that this interface needs to be updated every time a new product-related feature is implemented is a clear indication that something’s wrong.
If you extrapolate this to make a prediction, can you expect this interface to be updated again soon? The answer to that question is a clear “Yes!” As a matter of fact, Mary already has several features in her backlog, concerning cross-sellings, product pictures, and product reviews that would all cause changes to IProductService.4
What this teaches us is that, in this particular application, new features concerning products are added on a regular basis. Because this is an e-commerce application, this isn’t a world-shattering observation. But because this is both a central part of the code base and under frequent change, the need to improve the design arises. Let’s analyze the current design with SOLID principles in mind.
10.3.2 从SOLID角度分析IProductService
10.3.2 Analysis of IProductService from the perspective of SOLID
Concerning the five SOLID principles discussed in section 10.2, Mary’s design violates three out of five SOLID principles, namely, the ISP, SRP, and OCP. We’ll start with the first one: IProductService violates the ISP.
There’s one obvious violation — IProductService violates the ISP. As explained in section 10.2.4, the ISP prescribes the use of fine-grained Abstractions over wide Abstractions. From the perspective of the ISP, IProductService is rather wide. With listing 10.4 in mind, it’s easy to believe that there’ll be no single consumer of IProductService that’ll use all its methods. Most consumers would typically use one method or a few at most. But how is this violation a problem?
宽接口直接导致问题的代码库的一部分是在测试期间。HomeController例如,'s 的单元测试将定义一个IProductServiceTest Double 实现,但需要这样一个 Test Double 来实现其所有成员,即使它HomeController本身只使用一种方法。5 即使您可以创建可重用的测试替身,您通常仍希望断言 的不相关方法IProductService未被调用HomeController。以下清单显示了一个 MockIProductService实现,它断言未调用意外方法。
A part of the code base where wide interfaces directly cause trouble is during testing. HomeController’s unit tests, for instance, will define an IProductService Test Double implementation, but such a Test Double is required to implement all its members, even though HomeController itself only uses one method.5 Even if you could create a reusable Test Double, you typically still want to assert that unrelated methods of IProductService aren’t called by HomeController. The following listing shows a Mock IProductService implementation that asserts unexpected methods aren’t called.
Listing 10.5 A reusable Mock IProductService base class
public abstract class MockProductService : IProductService
{
public virtual void DeleteProduct(Guid productId)
{
Assert.True(false, "Should not be called."); ①
}
public virtual Product GetProductById(Guid id)
{
Assert.True(false, "Should not be called."); ①
return null;
}
public virtual void InsertProduct(Product product)
{
Assert.True(false, "Should not be called."); ①
}
... ②
}
All methods are implemented to fail by calling Assert.True using a value of false. The Assert.True method is part of the xUnit testing framework.6 By passing false, the assertion fails, and the currently running test also fails.
To preserve precious trees, listing 10.5 only shows a few of MockProductService’s methods, but we think you get the picture. You wouldn’t have to implement this big list of failing methods if the interface was specific to HomeController’s needs; in that case, HomeController is expected to call all its Dependency’s methods, and you wouldn’t have to do this check.
Because the ISP is the conceptual underpinning of the SRP, an ISP violation typically indicates an SRP violation in its implementations, as is the case here. SRP violations can sometimes be hard to detect, and you might argue that a ProductService implementation has one responsibility, namely, handling product-related use cases.
The concept of product-related use cases, however, is extremely vague and broad. Rather, you want classes that have only one reason to change. ProductService definitely has multiple reasons to change. For instance, any of the following reasons causes ProductService to change:
折扣应用方式的变化
Changes to how discounts are applied
更改库存调整的处理方式
Changes to how inventory adjustments are processed
Not only does ProductService have many reasons to change, its methods are most likely not cohesive. A simple way to spot low cohesion is to check how easy it is to move some of the class’s functionality to a new class. The easier this is, the lower the relatedness of the two parts, and the more likely SRP is violated.
Perhaps UpdateHasTierPricesProperty and UpdateHasDiscountsApplied share the same Dependencies, but that’d be about it; they aren’t cohesive. As a result, the class will likely be complex, which can cause maintainability problems. ProductService should, therefore, be split into multiple classes. But that raises this question: how many classes and which methods should be grouped together, if any? Before we get into that, let’s first inspect how the design around IProductService violates the OCP.
To test whether the code violates the OCP, you first have to determine what kind of changes to this part of the application you can expect. After that, you can ask the question, “Does this design cause sweeping changes when expected changes are made?”
You can expect two quite likely changes to happen during the course of the lifetime of the e-commerce application. First, new features will need to be added (Mary already has them on her backlog). Second, Mary likely also needs to apply Cross-Cutting Concerns. With these expected changes, the obvious answer to the question is, “Yes, the current design does cause sweeping changes.” Sweeping changes happen both when adding new features and when adding new aspects.
当添加一个与产品相关的新功能时,更改会波及所有IProductService实现,这将是主要ProductService实现,以及所有装饰器和测试替身。添加新的横切关注点时,系统也可能会发生涟漪变化,因为除了添加新的 Decorator for 之外IProductService,您还将添加 Decorators for ICustomerService,IOrderService和所有其他I...Service抽象。因为每个抽象可能包含许多方法,所以方面的代码会重复很多次,正如我们在 10.1 节中讨论的那样。
When a new product-related feature is added, the change ripples through all IProductService implementations, which will be the main ProductService implementation, and also all Decorators and Test Doubles. When a new Cross-Cutting Concern is added, there’ll likely be rippling changes to the system too, because, besides adding a new Decorator for IProductService, you’ll also be adding Decorators for ICustomerService, IOrderService, and all other I...ServiceAbstractions. Because each Abstraction potentially contains dozens of methods, the aspect’s code would be repeated many times, as we discussed in section 10.1.
在表 9.1 中,我们总结了您可能需要实施的各种可能方面。在项目开始时,您可能不知道需要哪些。但是,即使您可能不确切地知道您可能需要添加哪些横切关注点,假设您确实需要在项目过程中添加一些横切关注点是一个相当有根据的猜测,就像 Mary 所做的那样。
In table 9.1, we summed up a wide range of possible aspects you might need to implement. At the start of a project, you might not know which ones you’ll need. But even though you might not know exactly which Cross-Cutting Concerns you may need to add, it’d be a fairly well-educated guess to assume that you do need to add some during the course of the project, as Mary does.
From the previous analysis, you can conclude that, together with its implementations, listing 10.4 violates three out of five SOLID principles. Although from the perspective of AOP, you might be tempted to use either dynamic Interception (section 11.1) or compile-time weaving tools (section 11.2) to apply aspects, we argue that this only solves part of the problem; namely, how to effectively apply Cross-Cutting Concerns in a maintainable fashion. The use of tools doesn’t fix the underlying design issues that still cause maintainability problems in the long run.
正如我们将在 11.1.2 和 11.2.2 节中讨论的那样,AOP 的两种方法都有它们自己特定的一组缺点。但是让我们看看我们是否可以通过 Mary 的应用程序获得更SOLID和可维护的设计。
As we’ll discuss in sections 11.1.2 and 11.2.2, both methods of AOP have their own particular sets of disadvantages. But let’s take a look at whether we can get to a more SOLID and maintainable design with Mary’s app.
10.3.3 通过应用SOLID原则改进设计
10.3.3 Improving design by applying SOLID principles
在本节中,我们将通过执行以下操作逐步改进应用程序的设计:
In this section, we’ll improve the application’s design step by step by doing the following:
将读取与写入分开
Separate the reads from the writes
通过拆分接口和实现来修复 ISP 和 SRP 违规
Fix the ISP and SRP violations by splitting interfaces and implementations
通过引入参数对象和实现的通用接口来修复 OCP 违规
Fix the OCP violation by introducing Parameter Objects and a common interface for implementations
通过定义通用抽象来修复意外引入的 LSP 违规
Fix the accidentally introduced LSP violation by defining a generic Abstraction
第 1 步:将读取与写入分开
Step 1: Separating reads from writes
Mary 当前设计的一个问题是应用到的大多数方面IProductService仅是其方法的一个子集所需要的。尽管安全性等方面通常适用于所有功能,但审计、验证和容错等方面通常只需要应用程序中更改状态的部分。另一方面,诸如缓存之类的方面可能仅对读取数据而不更改状态的方法有意义。IProductService您可以通过拆分为只读和只写接口来简化 Decorator 的创建,如图 10.1所示。
One of the problems with Mary’s current design is that the majority of aspects applied to IProductService are only required by a subset of its methods. Although an aspect such as security typically applies to all features, aspects such as auditing, validation, and fault tolerance will usually only be required around the parts of the application that change state. An aspect such as caching, on the other hand, may only make sense for methods that read data without changing state. You can simplify the creation of Decorators by splitting IProductService into a read-only and write-only interface, as shown in figure 10.1.
The advantage of this split is that the new interfaces are finer-grained than before. This reduces the risk of you having to depend on methods that you don’t need. When you create a Decorator that applies a transaction to the executed code, for instance, only IProductCommandServices will need to be decorated, which eliminates the need to implement the IProductQueryServices’s methods. It also makes the implementations smaller and simpler to reason about.
Although this split is an improvement over the original IProductService interface, this new design still causes sweeping changes. As before, implementing a new product-related feature causes a change to many classes in the application. Although you reduced the likelihood of a class being changed by half, a change still causes about the same amount of classes to be touched. This brings us to the second step.
第 2 步:通过拆分接口和实现来修复 ISP 和 SRP
Step 2: Fixing ISP and SRP by splitting interfaces and implementations
Because splitting the wide interface pushes us in the right direction, let’s take this a step further. We’ll focus our attention on IProductCommandServices and ignore IProductQueryServices.
Let’s try something radical here. Let’s break up IProductCommandServices into multiple one-membered interfaces. Figure 10.2 shows how the ProductCommandServices implementation is segregated into seven classes, each with their own one-membered interface.
In figure 10.2 , you moved each method of the IProductCommandServices interface into a separate interface and gave each interface its own class. Listing 10.6 shows a few of those interface definitions.
Figure 10.2 The IProductCommandServices interface containing seven members is replaced with seven, one-membered interfaces. Each interface gets its own corresponding implementation.
When you create a one-to-one mapping from interface to implementation, each use case in the application gets its own class. This makes classes small and focused — they have a single responsibility.
添加新功能意味着添加新的接口实现对。无需对实现其他用例的现有类进行任何更改。
Adding a new feature means the addition of a new interface-implementation pair. No changes have to be made to existing classes that implement other use cases.
尽管这个新设计符合 ISP 和 SRP,但在创建装饰器时仍然会引起彻底的变化。这是如何做:
Even though this new design conforms to the ISP and the SRP, it still causes sweeping changes when it comes to creating Decorators. Here’s how:
With the IProductCommandServices interface split into seven, one-membered interfaces, there’ll be seven Decorator implementations per aspect. With 10 aspects, for instance, this means 70 Decorators.
对现有方面进行更改会导致对大量类的全面更改,因为每个方面都分布在许多装饰器上。
Making changes to an existing aspect causes sweeping changes throughout a large set of classes, because each aspect is spread out over many Decorators.
This new design causes each class in the application to be focused around one particular use case, which is great from the perspective of the SRP and the ISP. But, because these classes have no commonality to which you can apply aspects, you’re forced to create many Decorators with almost identical implementations. It’d be nice if you were able to define a single interface for all command operations in the code base. That would greatly reduce the code duplication around aspects and the number of Decorator classes to one Decorator per aspect.
When you look at listing 10.6, it might be hard to see how these interfaces have any similarity. They all return void, but all have a differently named method, and each method has a different set of parameters. There’s no commonality to extract from that — or is there?
What if you extract the method parameters of each command method into a Parameter Object? Most refactoring tools allow such refactoring with a few simple keystrokes.
下一个清单显示了这次重构的结果。
The next listing shows the result of this refactoring.
Listing 10.7 Wrapping method parameters in a Parameter Object
public interface IAdjustInventoryService
{
void Execute(AdjustInventory command); ①
}
public class AdjustInventory ② { ② public Guid ProductId { get; set; } ② public bool Decrease { get; set; } ② public int Quantity { get; set; } ② } ②
public interface IUpdateProductReviewTotalsService
{
void Execute(UpdateProductReviewTotals command); ③
}
public class UpdateProductReviewTotals ③ { ③ public Guid ProductId { get; set; } ③ public ProductReview[] Reviews { get; set; } ③ } ③
It’s important to note that even though both AdjustInventory and UpdateProductReviewTotals Parameter Objects are concrete objects, they’re still part of their Abstraction. As we mentioned in section 3.1.1, because they’re mere data objects without behavior, hiding their values behind an Abstraction would be rather useless. If you moved the implementations into a different assembly, the Parameter Objects would stay in the same assembly as their Abstraction. Also, these extracted Parameter Objects become the definition of a command operation. We therefore typically refer to these objects themselves as commands.
Both the InsertProduct and UpdateHasTierPricesProperty commands will have a single parameter of type Product. Inserting a product, however, is something completely different than updating a product’s HasTierPrices property. Again, the command type itself becomes the definition of a command operation.
With these refactorings, you effectively changed the code from 1 interface and implementation with 7 methods, to 7 interfaces and 14 classes. At this point, you might think we’re certifiably nuts and perhaps you’re ready to toss this book out the window. This might be the mental discomfort we warned about at the beginning of this section. Bear with us, because increasing the number of classes in your system might not be as bad as it might seem at first, and this refactoring will get us somewhere. Promise.
通过前面的重构,出现了一种模式:
With the previous refactoring, a pattern emerges:
每个抽象都包含一个方法。
Every Abstraction contains a single method.
每个方法都被命名Execute。
Every method is named Execute.
每个方法都返回void。
Every method returns void.
每种方法都有一个输入参数。
Every method has one single input parameter.
您现在可以从此模式中提取一个通用接口。这是如何做:
You can now extract a common interface from this pattern. Here’s how:
public interface ICommandService ①
{
void Execute(object command);
}
If you implement the command services using this new ICommandService interface, it results in the code in listing 10.8. Note that this new interface definition can likely be used to replace other I...ServiceAbstractions too.
public class AdjustInventoryService : ICommandService ①
{
readonly IInventoryRepository repository;
public AdjustInventoryService( ②
IInventoryRepository repository)
{
this.repository = repository;
}
public void Execute(object cmd)
{
var command = (AdjustInventory)cmd; ③ Guid id = command.ProductId; ④ bool decrease = command.Decrease; ④ int quantity = command.Quantity; ④ ... ④
}
}
Figure 10.3 shows how the number of interfaces are reduced from seven back to one. Now, however, you extract the method parameters into a Parameter Object per service.
As we stated previously, the Parameter Objects are part of the Abstraction. Collapsing all interfaces into one single interface makes this even more apparent. The Parameter Object has become the definition of a use case — it has become the contract. Consumers can get this ICommandService injected into their constructor and call its Execute method by supplying the appropriate Parameter Object.
The AdjustInventoryViewModel wraps the AdjustInventory command as a property. This is convenient, because AdjustInventory is part of the Abstraction and only contains data specific to the use case. AdjustInventory will be model-bound by the MVC framework, together with its surrounding AdjustInventoryViewModel, when the user posts back the request.
ICommandService用于实施横切关注点
Using ICommandService to implement Cross-Cutting Concerns
Having a single interface for all your command service calls in the code base provides a huge advantage. Because all the application’s state-changing use cases now implement this single interface, you can now create a single Decorator per aspect and wrap it around each and every implementation. To prove this point, the following listing shows the implementation of a transaction aspect as a Decorator for the ICommandService.
Using this new Decorator, you can now compose an InventoryController by injecting a new AdjustInventoryService that gets Intercepted by a Transaction-CommandServiceDecorator:
ICommandService service =
new TransactionCommandServiceDecorator(
new AdjustInventoryService(repository));
new InventoryController(service);
This design effectively prevents sweeping changes both when new features are added and when new Cross-Cutting Concerns need to be applied. This design is now truly closed for modification because
Adding a new (command) feature means creating a new command Parameter Object and a supporting ICommandService implementation. No existing classes need to be changed.
添加新功能不会强制创建新装饰器,也不会强制更改现有装饰器。
Adding a new feature doesn’t force the creation of new Decorators nor the change of existing Decorators.
可以通过添加单个装饰器来向应用程序添加新的横切关注点。
Adding a new Cross-Cutting Concern to the application can be done by adding a single Decorator.
更改横切关注点会导致更改单个类。
Changing a Cross-Cutting Concern results in changing a single class.
Some developers argue against having this many classes in their system, because they feel it complicates navigating through the project. This, however, only happens when you don’t structure your project properly. In this example, all product-related operations can be placed in a namespace called MyApp.Services.Products, effectively grouping those operations together, similar to what Mary’s IProductService did. Instead of having the grouping at the class level, you now have it at the project level, which is a great benefit, because the project structure immediately shows you the application’s behavior.
Now that you’ve fixed the previously analyzed SOLID violations, you might think that we’re done with our refactoring. But, unfortunately, these changes accidentally introduced a new SOLID violation. Let’s look at that next.
As mentioned, the definition of ICommandService accidentally introduced a new SOLID violation, namely, the LSP. The InventoryController of listing 10.9 exhibits this violation.
As we discussed in section 10.2.3, the LSP says that you must be able to substitute an Abstraction for an arbitrary implementation of that same Abstraction without changing the correctness of the client. According to the LSP, because the AdjustInventoryService implements the ICommandService, you should be able to substitute it for a different implementation without breaking the InventoryController. The following listing shows an altered object composition for InventoryController.
ICommandService service =
new TransactionCommandServiceDecorator(
new UpdateProductReviewTotalsService( ①
repository));
new InventoryController(service);
下面展示Execute方法对于:UpdateProductReviewTotalsService
The following shows the Execute method for UpdateProductReviewTotalsService:
public void Execute(object cmd)
{
var command = (UpdateProductReviewTotals)cmd; ①
...
}
InventoryController gets an ICommandService injected into its constructor. It passes on the AdjustInventory command to that injected ICommandService. Because the injected ICommandService is an UpdateProductReviewTotalsService, it’ll try to cast the incoming command to UpdateProductReviewTotals. Because it’ll be unable to cast AdjustInventory to UpdateProductReviewTotals, however, the cast fails. This breaks InventoryController and therefore violates the LSP.
Although one could argue that it’s up to the Composition Root to supply the correct implementation, the ICommandService interface still causes ambiguity, and it prevents the compiler from verifying whether the composition of our object graph makes sense. LSP violations tend to make a system fragile. Furthermore, the untyped command method argument that Execute methods consume requires every ICommandService implementation to contain a cast, which can be considered a code smell in its own right. Let’s fix this violation.
You might be confused as to how making the interface generic helps. To help clarify this, the next listing shows how you would implement ICommandService<TCommand>.
public class AdjustInventoryService
: ICommandService<AdjustInventory> ①
{
private readonly IInventoryRepository repository;
public AdjustInventoryService(
IInventoryRepository repository)
{
this.repository = repository;
}
public void Execute(AdjustInventory command) ②
{
var productId = command.ProductId; ③ ③ ... ③
}
}
许多框架和在线参考体系结构示例对类似于前面示例的接口有不同的名称。它们可能被命名为IHandler<T>、ICommandHandler<T>、IMessageHandler<T>或IHandleMessages<T>。一些抽象是异步的并返回 a Task,而其他抽象则添加 aCancellationToken作为方法参数。有时该方法称为Handleor HandleAsync。尽管命名不同,但它的思想及其对应用程序可维护性的影响是相同的。
Many frameworks and online reference architecture samples have different names for an interface similar to the previous examples. They might be named IHandler<T>, ICommandHandler<T>, IMessageHandler<T>, or IHandleMessages<T>. Some Abstractions are asynchronous and return a Task, whereas others add a CancellationToken as a method argument. Sometimes the method is called Handle or HandleAsync. Although named differently, the idea and the effect it has on the maintainability of your application, however, is the same.
Although the additional compile-time support in the implementation is certainly a nice plus, the main reason for the generic ICommandService<TCommand> is to prevent violating the LSP in its clients. The following listing shows how injecting ICommandService<TCommand> into the InventoryController fixes the LSP.
Changing the non-generic ICommandService into the generic ICommandService<TCommand> fixes our last SOLID violation. This would be a good time to reap the benefits of our new design.
使用通用抽象应用事务处理
Applying transaction handling using the generic Abstraction
Although there’s more to a generic one-membered Abstraction than just Cross-Cutting Concerns, the ability to apply aspects in a way that doesn’t cause sweeping changes is one of the greatest benefits of such a design. As with the non-generic ICommandService interface, ICommandService<TCommand> still allows the creation of a single Decorator per aspect. Listing 10.15 shows a rewrite of the transaction Decorator of listing 10.10 using the new generic ICommandService<TCommand>Abstraction.
Using the ICommandService<TCommand> interface and the TransactionCommandServiceDecorator<TCommand> Decorator, your Composition Root becomes the following:
new InventoryController(
new TransactionCommandServiceDecorator<AdjustInventory>(
new AdjustInventoryService(repository)));
This brings us to the point where this one-membered generic Abstraction starts to steal the show. This is when you start adding more Cross-Cutting Concerns.
The examples of Cross-Cutting Concerns we discussed in section 9.2 all focused on applying aspects at the boundary of Repositories (such as in listings 9.4 and 9.7). In this section, however, we shift the focus one level up in the layered architecture, from the data access library’s repository to the domain library’s IProductService.
This shift is deliberate, because you’ll find that Repositories aren’t the right granular level for applying many Cross-Cutting Concerns effectively. A single business action defined in the domain layer would potentially call multiple Repositories, or call the same Repository multiple times. If you were to apply, for instance, a transaction at the level of the repository, it’d still mean that the business operation could potentially run in dozens of transactions, which would endanger the correctness of the system.
单个业务操作通常应在单个事务中运行。这种粒度级别不仅适用于事务,也适用于其他类型的操作。
A single business operation should typically run in a single transaction. This level of granularity holds not only for transactions, but other types of operations as well.
The domain library implements business operations, and it’s at this boundary that you typically want to apply many Cross-Cutting Concerns. The following lists some examples. It isn’t a comprehensive listing, but it’ll give you a sense of what you could apply on that level:
Auditing — Although you could implement auditing around Repositories, as you did in the AuditingUserRepositoryDecorator of listing 9.1, this presents a list of changes to individual Entities, and you lose the overall picture — that is, why the change happened. Reporting changes to individual Entities might be suited for CRUD-based applications, but if the application implements more-complex use cases that influence more than a single Entity, it becomes beneficial to pull auditing a level up and store information about the executed command. We’ll show an auditing example next.
Logging — As we alluded to in section 5.3.2, a good application design can prevent unnecessary logging statements spread across the entire code base. Logging any executed business operation with its data provides you with detailed information about the call, which typically removes the need to log at the start of each method.
Performance monitoring — Since 99% of the time executing a request is typically spent running the business operation itself, ICommandService<TCommand> becomes an ideal boundary for plugging in performance monitoring.
Security — Although you might try to restrict access on the level of the repository, this is typically too fine-grained, because you more likely want to restrict access at the level of the business operation. You can mark your commands with either a permitted role or a permission, which makes it trivial to apply security concerns around all business operations using a single Decorator. We’ll show an example shortly.
Fault tolerance — Because you want to apply transactions around your business operations, as we’ve shown in listing 10.15, other fault-tolerant aspects should typically be applied on the same level. Implementing a database deadlock retry aspect, for instance, is a good example. Such a mechanism should always be applied around a transaction aspect.
Validation — As we demonstrated in listings 10.9 and 10.14, the command can become part of the web request’s submitted data. By enriching commands with Data Annotations’ attributes, the command’s data will also be validated by MVC.9 As an extra safety measure, you can create a Decorator that validates an incoming command using Data Annotations’ static Validator class.10
以下部分将介绍如何在ICommandService<TCommand>.
The following sections take a look at how you can implement two of these aspects on top of ICommandService<TCommand>.
Listings 9.1 and 9.2 defined an auditing Decorator for IUserRepository, while reusing the IAuditTrailAppender from listing 6.23. If you apply auditing on ICommandService<TCommand> instead, you’re at the ideal level of granularity, because the command contains all interesting use case–specific data you might want to record. If you enrich this data and metadata with some contextual information, such as username and the current system time, you’re pretty much done. The next listing shows an auditing Decorator on top of ICommandService<TCommand>.
Listing 10.16 Implementing a generic auditing aspect for business operations
public class AuditingCommandServiceDecorator<TCommand>
: ICommandService<TCommand>
{
private readonly IUserContext userContext;
private readonly ITimeProvider timeProvider;
private readonly CommerceContext context;
private readonly ICommandService<TCommand> decoratee;
public AuditingCommandServiceDecorator(
IUserContext userContext,
ITimeProvider timeProvider, ①
CommerceContext context,
ICommandService<TCommand> decoratee)
{
this.userContext = userContext;
this.timeProvider = timeProvider;
this.context = context;
this.decoratee = decoratee;
}
public void Execute(TCommand command)
{
this.decoratee.Execute(command);
this.AppendToAuditTrail(command);
}
private void AppendToAuditTrail(TCommand command)
{
var entry = new AuditEntry ② { ② UserId = this.userContext.CurrentUser.Id, ② TimeOfExecution = this.timeProvider.Now, ② Operation = command.GetType().Name, ② Data = Newtonsoft.Json.JsonConvert ② .SerializeObject(command) ②
};
this.context.AuditEntries.Add(entry);
this.context.SaveChanges();
}
}
当 Mary 使用 运行应用程序时,装饰器会在审计表中生成信息,如表 10.2所示。AuditingCommandServiceDecorator<TCommand>
When Mary runs the application using the AuditingCommandServiceDecorator<TCommand>, the Decorator produces the information in the auditing table, shown in table 10.2.
As stated previously, AuditingCommandServiceDecorator<TCommand> uses reflection to get the name of the command and convert the command to a JSON format. Although JSON is human readable, you probably don’t want to show this to your end users. Still, this is a good format to use for backend auditing purposes. Using this information, you’ll be able to efficiently see what happened in your system, by whom, and at which point in time. It would even allow you to replay an operation if it failed for some reason or to use this information to perform a realistic stress test on the system. You could deserialize the information from this table back to commands and run them through the system.
As we described in section 6.3.2, domain events are another well-suited technique that can also be used for auditing. This auditing aspect, however, only records a user’s successful action. Although an auditor might not be interested in failures, we as developers certainly are. It isn’t hard to imagine how you’d use the same mechanism to record the same data and include a stack trace when the operation fails.
Likewise, you can use this information for performance monitoring in the same way, where you store an additional timespan next to the time and the operation details. This easily allows you to monitor which operations become slower over time. Before showing you an example of the new Composition Root with AuditingCommandServiceDecorator<TCommand> applied, we’ll first take a look at how you can use passive attributes to implement a security aspect.
During our discussion about Cross-Cutting Concerns in section 9.2, you implemented a SecureProductRepositoryDecorator in listing 9.7. Because that Decorator was specific to IProductRepository, it was clear what role the Decorator should grant access to. In the example, access to the write methods of IProductRepository was restricted to the Administrator role.
With this new generic model, a single Decorator is wrapped around all business operations, not just the product CRUD operations. Some operations also need to be executable by other roles, which makes the hard-coded Administrator role unsuited for this generic model. You can implement such a security check on top of a generic Abstraction in many ways, but one compelling method is through the use of passive attributes.
public class PermittedRoleAttribute : Attribute ①
{
public readonly Role Role;
public PermittedRoleAttribute(Role role) ②
{
this.Role = role;
}
}
public enum Role ③
{
PreferredCustomer,
Administrator,
InventoryManager
}
您可以使用此属性通过有关允许哪个角色执行操作的元数据来丰富命令。
You can use this attribute to enrich commands with metadata about which role is allowed to execute an operation.
Listing 10.18 Enriching commands with security-related metadata
[PermittedRole(Role.InventoryManager)] ①
public class AdjustInventory
{
public Guid ProductId { get; set; }
public bool Decrease { get; set; }
public int Quantity { get; set; }
}
[PermittedRole(Role.Administrator)] ①
public class UpdateProductReviewTotals
{
public Guid ProductId { get; set; }
public ProductReview[] Reviews { get; set; }
}
There’s a big difference between applying aspect attributes, as we’ll discuss in section 11.2, and a passive attribute, such as the PermittedRoleAttribute. Compared to aspect attributes, passive attributes are decoupled from the aspect that use their values, which is one of the main problems with compile-time weaving, as you’ll see in chapter 11. The passive attribute doesn’t have a direct relationship with the aspect. This allows the metadata to be reused by multiple aspects, perhaps in different ways.
Like you’ve seen previously, adding the security behavior is a matter of creating the Decorator and wrapping it around the real implementation. Listing 10.19 shows such a Decorator. It makes use of the PermittedRoleAttribute that’s supplied to commands, as listing 10.18 showed.
We could give you tons of examples of Decorators that can be wrapped around business transactions, but there’s a limit to the number of pages a book can have. Besides, at this point, we think you’re starting to get the picture about how to apply Decorators on top of ICommandService<TCommand>. Let’s piece everything together inside the Composition Root.
In the previous sections, you declared three Decorators implementing security, transaction management, and auditing. You need to apply these Decorators around a real implementation in your Composition Root. Figure 10.4 shows how the Decorators are wrapped around a command service like a set of Russian nesting dolls.
ICommandService<AdjustInventory> service =
new SecureCommandServiceDecorator<AdjustInventory>(
this.userContext,
new TransactionCommandServiceDecorator<AdjustInventory>(
new AuditingCommandServiceDecorator<AdjustInventory>(
this.userContext,
this.timeProvider,
context,
new AdjustInventoryService(repository))));
return new InventoryController(service);
Because the application is expected to get many ICommandService<TCommand> implementations, most of the implementations would require the same decorators. Listing 10.20, therefore, would lead to lots of code repetition inside the Composition Root. This is something that’s easily fixed by extracting the repeated Decorator creation into its own method.
Extracting the Decorators into the Decorate method allows the Composition Root to be completely DRY. The creation of AdjustInventoryService is reduced to a simple one-liner:
var service = Decorate(new AdjustInventoryService(repository), context);
return new InventoryController(service);
Chapter 12 demonstrates how to Auto-RegisterICommandService<TCommand> implementations and apply Decorators using a DI Container. Because this almost brings us to the end of this section about using SOLID principles as a driver for AOP, let’s reflect for a moment on what we’ve achieved and how this relates to the bigger picture of application design.
10.3.5 结论
10.3.5 Conclusion
在本章中,您将IProductService包含多个命令方法的领域层的 big 重构为单个ICommandService<TCommand>抽象,其中每个命令都有自己的消息和用于处理该消息的相关实现。这种重构并没有改变任何原始的应用程序逻辑;但是,您确实明确了命令的概念。
In this chapter, you refactored the domain layer’s big IProductService, which consisted of several command methods, into a single ICommandService<TCommand>Abstraction, where each command got its own message and associated implementation for handling that message. This refactoring didn’t change any of the original application logic; you did, however, make the concept of commands explicit.
一个重要的观察是,这些域命令现在作为系统中的一个明确的工件公开,并且它们的处理程序被标记为单一接口。这种方法类似于您在使用 ASP.NET Core MVC 等应用程序框架时隐式练习的方法。MVC 控制器通常是通过继承Controller抽象来定义的;这允许 MVC 使用反射找到它们,并且它提供了一个用于与它们交互的通用 API。这种做法在应用程序设计中的更大范围内是有价值的,正如您在这些命令中看到的那样,您在这些命令中为它们的处理程序提供了一个通用 API(单一Execute方法)。这允许有效地应用方面并且没有代码重复。
An important observation is that these domain commands are now exposed as a clear artifact in the system, and their handlers are marked with a single interface. This methodology is similar to what you implicitly practice when working with application frameworks such as ASP.NET Core MVC. MVC Controllers are typically defined by inheriting from the ControllerAbstraction; this allows MVC to find them using reflection, and it presents a common API for interacting with them. This practice is valuable at a larger scale in the application’s design, as you’ve seen with these commands where you gave their handlers a common API (a single Execute method). This allowed aspects to be applied effectively and without code repetition.
Besides commands, there are other artifacts in the system that you might want to design in a similar fashion in order to be able to apply Cross-Cutting Concerns. A common artifact that deserves to be exposed more clearly is that of a query. At the start of section 10.3.3, after you split up IProductService into a read and write interface, we focused your attention on IProductCommandServices and ignored IProductQueryServices. Queries deserve an Abstraction of their own. Due to space constraints, however, a discussion of this is outside the scope of this book.15
Our point, however, is that in many types of applications, it’s possible to determine a commonality between groups of related components as you did in this chapter. This might help with applying Cross-Cutting Concerns more effectively and also supplies you with an explicit and compiler-verified coding convention.
But the goal of this chapter wasn’t to state that the ICommandService<TCommand>Abstraction is the way to design your applications. The important takeaway from this chapter should be that designing applications according to SOLID is the way to keep applications maintainable. As we demonstrated, this can, for the most part, be achieved without the use of specialized AOP tooling. This is important, because those tools come with their own sets of limitations and problems, which is something we’ll go into deeper in the next chapter. We have found, however, a certain set of design structures to be applicable to many line-of-business (LOB) applications — an ICommandService-like Abstraction being one of them.
这并不意味着应用SOLID原则总是很容易。相反,这可能很困难。如前所述,这需要时间,而且您永远不会 100%可靠。作为软件开发人员,您的工作就是找到最佳点;在适当的时候应用 DI 和SOLID绝对会增加你接近它的机会。
This doesn’t mean that it’s always easy to apply SOLID principles. On the contrary, it can be difficult. As stated previously, it takes time, and you’ll never be 100% SOLID. Your job as software developers is to find the sweet spot; applying DI and SOLID at the right moments will absolutely boost your chances of getting closer to that.
DI shines when it comes to applying recognized object-oriented principles such as SOLID. In particular, the loosely coupled nature of DI lets you use the Decorator pattern to follow the OCP as well as the SRP. This is valuable in a wide range of situations, because it enables you to keep your code clean and well organized, especially when it comes to addressing Cross-Cutting Concerns.
But let’s not beat around the bush. Writing maintainable software is hard, even when you try to apply the SOLID principles. Besides, you often work in projects that aren’t designed to stand the test of time. It might be unfeasible or dangerous to make big architectural changes. At those times, using AOP tooling might be your only viable option, even if it presents you with a temporary solution. Before you decide to use these tools, it’s important to understand how they work and what their weaknesses are, especially compared to the design philosophy described in this chapter. This will be the subject of the next chapter.
The Single Responsibility Principle (SRP) states that each class should have only one reason to change. This can be viewed from the perspective of cohesion. Cohesion is defined as the functional relatedness of the elements of a class or module. The lower the amount of relatedness, the lower the cohesion; and the lower the cohesion, the greater the chance a class violates the SRP.
The Open/Closed Principle (OCP) prescribes an application design that prevents you from having to make sweeping changes throughout the code base. A strong relationship between the OCP and the DRY principle is that they both strive for the same objective.
不要重复自己 (DRY) 原则指出,每条知识都必须在系统中具有单一、明确、权威的表示形式。
The Don’t Repeat Yourself (DRY) principle states that every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
Liskov 替换原则(LSP) 指出每个实现都应按照其抽象定义的方式运行。这使您可以用相同抽象的另一个实现替换最初预期的实现,而不必担心破坏消费者。这是DI的基础。当消费者不观察它时,注入Dependencies几乎没有优势,因为您不能随意替换Dependencies,并且您会失去 DI 的许多(如果不是全部)好处。
The Liskov Substitution Principle (LSP) states that every implementation should behave as defined by its Abstraction. This lets you replace the originally intended implementation with another implementation of the same Abstraction without worrying about breaking a consumer. It’s a foundation of DI. When consumers don’t observe it, there’s little advantage in injecting Dependencies, because you can’t replace Dependencies at will, and you lose many (if not all) benefits of DI.
The Interface Segregation Principle (ISP) promotes the use of fine-grained Abstractions rather than wide Abstractions. Any time a consumer depends on an Abstraction where some of the members stay unused, this principle is violated. This principle is crucial when it comes to effectively applying Aspect-Oriented Programming.
The Dependency Inversion Principle (DIP) states that you should program against Abstractions and that the consuming layer should be in control of the shape of a consumed Abstraction. The consumer should be able to define the Abstraction in a way that benefits itself the most.
These five principles together form the SOLID acronym. None of the SOLID principles represents absolutes. They’re guidelines that can help you write clean code.
面向方面的编程(AOP) 是一种范例,它侧重于有效且可维护地应用横切关注点的概念。
Aspect-Oriented Programming (AOP) is a paradigm that focuses on the notion of applying Cross-Cutting Concerns effectively and maintainably.
The most compelling AOP technique is SOLID. A SOLID application prevents code duplication during normal application code and implementation of Cross-Cutting Concerns. Using SOLID techniques can also help developers avoid the use of specific AOP tooling.
Even with a SOLID design, there likely will come a time where a change becomes sweeping. Being 100% closed for modification is neither possible nor desirable. Conforming to the OCP takes considerable effort when finding and designing the appropriate Abstractions, although too many Abstractions can have a negative impact on the complexity of the application.
Command-Query Separation (CQS) is an influential object-oriented principle that states that each method should either return a result but not change the observable state of the system, or change the state but not produce any value.
Placing command methods and query methods in different Abstractions simplifies applying Cross-Cutting Concerns, because the majority of aspects need to be applied to either commands or queries, but not both.
A Parameter Object is a group of parameters that naturally go together. The extraction of Parameter Objects allows the definition of a reusable Abstraction that can be implemented by a large group of components. This allows these components to be handled similarly and Cross-Cutting Concerns to be applied effectively.
这些提取的参数对象不是组件的抽象,而是成为系统中不同操作或用例的定义。
Rather than a component’s Abstraction, these extracted Parameter Objects become the definition of a distinct operation or use case in the system.
Although splitting larger classes into many smaller classes with Parameter Objects can drastically increase the number of classes in a system, it can also dramatically improve the maintainability of a system. The number of classes in a system is a bad metric for measuring maintainability.
Cross-Cutting Concerns should be applied at the right granular level in the application. For all but the simplest CRUD applications, Repositories aren’t the right granular level for most Cross-Cutting Concerns. With the application of SOLID principles, reusable one-membered Abstractions typically emerge as the levels where Cross-Cutting Concerns need to be applied.
第11
章 基于工具的面向切面编程
11
Tool-based Aspect-Oriented Programming
在这一章当中
In this chapter
使用动态拦截通过生成的装饰器应用拦截器
Using dynamic Interception to apply Interceptors using generated Decorators
动态拦截的优缺点
Advantages and disadvantages of dynamic Interception
使用编译时编织来应用横切关注点
Using compile-time weaving to apply Cross-Cutting Concerns
This chapter is a continuation of the Aspect-Oriented Programming (AOP) discussion that we started in chapter 10. Where chapter 10 described AOP in its purest form — namely, applying AOP solely using SOLID design practices — this chapter approaches AOP from a tool-based perspective. We’ll discuss two common methods for applying AOP: dynamic Interception and compile-time weaving.
In case the design approach of chapter 10 is too radical, dynamic Interception will be your next best pick, which is why we’ll discuss it first. Dynamic Interception might be a good temporary solution until the right time arrives to start making the kinds of improvements discussed in the last chapter.
编译时编织与 DI 相反,我们认为它是一种反模式。然而,我们认为包含关于编译时织入的讨论很重要,因为它是一种众所周知的 AOP 形式,而且我们想明确表示它不是 DI 的可行替代方案。
Compile-time weaving is the opposite of DI, and we consider it to be an anti-pattern. We feel it’s important, however, to include a discussion on compile-time weaving, because it’s a well-known form of AOP, and we want to make it clear that it isn’t a viable alternative to DI.
The code listings of section 10.1, which implement the Delete and Insert methods of CircuitBreakerProductRepositoryDecorator, contained code duplication. The following listing shows this code again.
Listing 11.1 Violating the DRY principle (repeated)
public void Delete(Product product)
{
this.breaker.Guard(); ① ① try ① { ① this.decoratee.Delete(product); ① this.breaker.Succeed(); ① } ① catch (Exception ex) ① { ① this.breaker.Trip(ex); ① throw; ① } ①
}
public void Insert(Product product)
{
this.breaker.Guard(); ① ① try ① { ① this.decoratee.Insert(product); ① this.breaker.Succeed(); ① } ① catch (Exception ex) ① { ① this.breaker.Trip(ex); ① throw; ① } ①
}
将 Decorator 作为方面实现的最困难的部分是设计模板。之后,这是一个相当机械的过程:
The hardest part of implementing a Decorator as an aspect is to design the template. After that, it’s a rather mechanical process:
新建装饰器类
Create a new Decorator class
从所需的界面派生
Derive from the desired interface
通过应用模板实现每个接口成员
Implement each interface member by applying the template
This process is so repetitive that you can use a tool to automate it. Among the many powerful features of the .NET Framework is the ability to dynamically emit types. This makes it possible to write code that generates a fully functional class at runtime. Such a class has no underlying source code file, but is compiled directly from some abstract model. This enables you to automate the generation of Decorators that are created at runtime. As figure 11.1 shows, this is what dynamic Interception enables you to do.
After the object graph for the dynamically generated Decorator and its Dependencies is created, the Decorator can be used as a stand-in for the real class. Because it implements the real class’s Abstraction, it can be injected into clients that use that Abstraction. Figure 11.2 describes the flow of method calls when the client calls into its Intercepted Abstraction.
Figure 11.1 A dynamic Interception library generates a Decorator class at runtime. This happens once per given Abstraction (in this case, for IRepo). After the generation process completes, you can request that the Interception library create new instances of that Decorator for you, while you supply both the target and the interceptor.
To use dynamic Interception, you must still write the code that implements the aspect. This could be the plumbing code required for the Circuit Breaker aspect as shown in listing 11.1. Once you’ve done this, you must tell the dynamic Interception library about the Abstractions it should apply the aspect to. Enough with the theory, let’s see an example.
11.1.1 示例:使用 Castle 动态代理进行拦截
11.1.1 Example: Interception with Castle Dynamic Proxy
With its repetitive code, the Circuit Breaker aspect from listing is a good candidate for dynamic Interception. While you can write the code that generates Decorators at runtime, this is a rather involved operation, and besides, there are already excellent tools available. Instead of taking you through the tedious process of generating code by hand, we’ll start using a tool directly. As an example, let’s see how you can reduce code duplication with Castle Dynamic Proxy’s Interception capabilities.
Implementing an Interceptor for Castle requires that you implement its Castle.DynamicProxy.IInterceptor interface, which consists of a single method. The following listing shows how to implement the Circuit Breaker from listing 11.1. Distinct from that listing, however, the following shows the entire class.
The main difference from listing 11.1 is that instead of delegating the method call to a specific method, you must be more general, because you apply this code to potentially any method. The IInvocation interface passed to the Intercept method as a parameter represents the method call. It might, for example, represent the call to the Insert(Product) method. The Proceed method is one of the key members of this interface, because it enables you to let the call proceed to the next implementation on the stack.
The IInvocation interface enables you to assign a return value before letting the call proceed. It also provides access to detailed information about the method call. From the invocation parameter, you can get information about the name and parameter values of the method, as well as other information about the current method call. Implementing the Interceptor is the hard part. The next step is easy.
使用纯 DI在组合根内应用拦截器
Applying the Interceptor inside the Composition Root using Pure DI
Listing 11.3 Incorporating the Interceptor into the Composition Root
var generator =
new Castle.DynamicProxy.ProxyGenerator(); ①
var timeout = TimeSpan.FromMinutes(1);
var breaker = new CircuitBreaker(timeout);
var interceptor =
new CircuitBreakerInterceptor(breaker); ② var wcfRepository = new WcfProductRepository(); ③ IProductRepository repository = generator ④ .CreateInterfaceProxyWithTarget<IProductRepository>( ④ wcfRepository, ④ interceptor); ④
This example shows that, although Castle is in control of the construction of the IProductRepository Decorator and the injection of its Dependencies, you can still bootstrap your application using Pure DI. In the next section, we’ll analyze dynamic Interception and discuss its advantages and disadvantages.
When we compare the tool-based AOP approach of dynamic Interception to the AOP by design approach discussed in the previous chapter, we find a number of similarities between the two:
当您针对抽象进行编程时,每一个都使您能够解决横切关注点。
Each enables you to address Cross-Cutting Concerns when you program against Abstractions.
与普通的旧装饰器一样,拦截器可以使用构造函数注入,这使它们对 DI 友好并且与它们正在装饰的代码分离。这些特性使您的业务代码和方面都可以轻松测试。
As with plain old Decorators, Interceptors can use Constructor Injection, which makes them DI-friendly and decoupled from the code they’re decorating. These characteristics allow both your business code and your aspects to be easily tested.
方面可以集中在Composition Root中,这可以防止代码重复,如果您的 Visual Studio 解决方案包含多个应用程序,它允许方面在一个Composition Root中应用,但不能在另一个中应用。
Aspects can be centralized in the Composition Root, which prevents code duplication, and in case your Visual Studio solution contains multiple applications, it allows the aspects to be applied in one Composition Root, but not the other.
Despite these similarities, there are some differences that make dynamic Interception less than ideal. Table 11.1 summarizes the downsides, which we’ll discuss next.
Compared to plain old Decorators, dynamic Interception involves a fair deal of runtime reflection calls any time an Interceptor is used. With Castle, for instance, the IInvocation interface contains an Arguments property that returns an array of object instances that contains the list of method arguments. Reading and changing those values involves casting and boxing in case of value types like integer and boolean. From a performance perspective, this constant burden of reflection will be, for the most part, negligible. Your typical I/O operations, such as database reads and writes, cost orders of magnitude more.
This use of reflection, however, does complicate the Interceptors you write. When handling the list of method arguments and return types, you’ll have to write the proper casting and type checking, and possibly communicate casting errors more effectively. An Interceptor, therefore, tends to be more complicated than a Decorator, which makes it harder to read and maintain.
Compared to plain old Decorators, the Interceptors you write with dynamic Interception are strongly coupled to the Interception library you use. The CircuitBreakerInterceptor of listing 11.2 is a good example of this. This Interceptor implements Castle.DynamicProxy.IInterceptor and makes use of the Castle.DynamicProxy.IInvocationAbstraction.
Although less pervasive than compile-time weaving, as you’ll see in section 11.2, this leads to all aspects being coupled to a Castle Dynamic Proxy library. This coupling introduces an extra dependency on an external library that needs to be learned, which brings extra costs and risks to the project. We’ll explain this in detail in section 12.3.1 .
Because dynamic Interception works by wrapping existing Abstractions with dynamically generated Decorators, the behavior of a class can only be extended at the Abstraction’s method boundaries. Private methods can’t be Intercepted because they’re not part of the interface.
This limitation also holds true when practicing AOP by design. With AOP by design, however, this is typically less of a problem, because you design your Abstractions in such a way that there’s only a need to apply aspects at the boundaries of the Abstractions.2 When you apply dynamic Interception, on the other hand, you typically accept the status quo because, if you didn’t, you’d end up practicing AOP by design.
In chapter 10, we extensively discussed the design problems that existed with the big IProductService interface and how they could be fixed by applying SOLID principles. As discussed, these problems have a bigger impact on the system beyond any issues regarding Cross-Cutting Concerns.
You can use dynamic Interception, however, when you accept the status quo of the application’s current design. You want to be able to apply Cross-Cutting Concerns without having to apply large refactorings. The disadvantage of this is that you only solve part of the problem. You’ll still end up with a system that’s only marginally more maintainable than the existing design and considerably less maintainable than a more SOLID-based design This is because dynamic Interception only considers the application of Cross-Cutting Concerns — not other parts of your code.
应用动态拦截要求您针对接口进行编程并使用第 4 章中的 DI 模式。另一种不需要针对接口进行编程的 AOP 形式是编译时织入。乍一看这听起来很有吸引力,但正如我们接下来要讨论的那样,它是一种 DI 反模式。
Applying dynamic Interception requires you to program to interfaces and to use the DI patterns from chapter 4. Another form of AOP that doesn’t require programming to interfaces is compile-time weaving. This may sound attractive at first, but as we’ll discuss next, it’s a DI anti-pattern.
11.2 编译时织入
11.2 Compile-time weaving
当我们作为开发人员编写 C# 代码时,C# 编译器会将我们的代码转换为 Microsoft 中间语言(IL). IL 由 Common 读取语言运行时 (CLR) 即时 (JIT) 编译器并当场翻译成机器指令供 CPU 执行。3 您很可能熟悉此过程的基础知识。
When we as developers write C# code, the C# compiler transforms our code to Microsoft Intermediate Language (IL). IL is read by the Common Language Runtime (CLR) Just-In-Time (JIT) compiler and is translated on the spot to machine instructions for execution by the CPU.3 You’ll most likely be familiar with the basics of this process.
Compile-time weaving is a common AOP technique that alters this compilation process. It uses special tools to read a compiled assembly produced by our (C#) compiler, modifies it, and writes it back to disk, effectively replacing the original assembly. Figure 11.3 shows this process.
Altering an originally compiled assembly in a post-compilation process is done with the intention of weaving aspects into the original source code, as shown in figure 11.4.
但是,尽管一开始看起来很诱人,但当应用于易失性依赖项时,编译时编织的使用会带来一些问题,从可维护性的角度来看,这种技术存在问题。由于这些缺点,正如本节中所解释的那样,我们认为编译时织入是 DI 的对立面——它是一种 DI 反模式。
But, as alluring as it may seem at first, when applied to Volatile Dependencies, the use of compile-time weaving comes with issues that make this technique problematic from a maintainability perspective. Because of these downsides, as explained throughout this section, we consider compile-time weaving to be the opposite of DI — it’s a DI anti-pattern.
正如我们在简介中所述,我们发现讨论编译时织入很重要,即使它是一种 DI 反模式。编译时编织是一种众所周知的 AOP 形式,我们必须警告不要使用它。在我们讨论为什么它有问题之前,我们将从一个例子开始。
As we stated in the introduction, we found it important to discuss compile-time weaving even though it’s a DI anti-pattern. Compile-time weaving is such a well-known form of AOP that we have to warn against its use. Before we discuss why it’s problematic, we’ll begin with an example.
11.2.1 示例:使用编译时编织应用事务切面
11.2.1 Example: Applying a transaction aspect using compile-time weaving
Attributes share a trait with Decorators: although they may add or imply a modification of behavior of a member, they leave the signature and original source code unchanged. In section 9.2.3, you applied a security aspect using a Decorator. Compile-time weaving tools, however, let you declare aspects by placing attributes on classes, their members, and even assemblies.
It sounds attractive to use this concept to apply Cross-Cutting Concerns. Wouldn’t it be nice if you could decorate a method or class with a [Transaction] attribute, or even a custom [CircuitBreaker] attribute and, in this way, apply the aspect with a single line of declarative code? The following listing shows how a custom TransactionAttribute aspect attribute gets applied directly to the methods of SqlProductRepository.
Listing 11.4 Applying a [Transaction] aspect attribute to SqlProductRepository
public class SqlProductRepository : IProductRepository
{
[Transaction] ①
public void Insert(Product product) ...
[Transaction] ①
public void Update(Product product) ...
[Transaction] ①
public void Delete(Guid id) ...
public IEnumerable<Product> GetAll() ... ③
...
}
Although there are many compile-time weaving tools you can choose from, in this section, we’ll use PostSharp (https://www.postsharp.net/), which is a commercial tool. The next listing shows the definition of TransactionAttribute using PostSharp.
Listing 11.5 Implementing a TransactionAttribute aspect with PostSharp
[AttributeUsage(AttributeTargets.Method | ① AttributeTargets.Class | ① AttributeTargets.Assembly, ① AllowMultiple = false)] ① [PostSharp.Serialization.PSerializable] ① [PostSharp.Extensibility.MulticastAttributeUsage( ① MulticastTargets.Method, ① TargetMemberAttributes = ① MulticastAttributes.Instance | ① MulticastAttributes.Static)] ①
public class TransactionAttribute
: PostSharp.Aspects.OnMethodBoundaryAspect ③
{
public override void OnEntry( ④ MethodExecutionArgs args) ④ { ④ args.MethodExecutionTag = ④ new TransactionScope(); ④ } ④ ④ public override void OnSuccess( ④ MethodExecutionArgs args) ④ { ④ var scope = (TransactionScope) ④ args.MethodExecutionTag; ④ scope.Complete(); ④ } ④ ④ public override void OnExit( ④ MethodExecutionArgs args) ④ { ④ var scope = (TransactionScope) ④ args.MethodExecutionTag; ④ scope.Dispose(); ④ } ④
}
因为您想围绕任意一段代码包装事务,所以您需要重写 的三个方法— 即、和。在 期间,您创建一个新的 ,在 期间,您处理范围。保证被调用。PostSharp 会将其调用包装在一个块中。只有当包装操作成功时,您才会调用该方法。这就是为什么你在方法中实现它OnMethodBoundaryAspectOnEntryOnSuccessOnExitOnEntryTransactionScopeOnExitOnExitfinallyCompleteOnSuccess. 你利用MethodExecutionTag财产将创建TransactionScope的 from 方法转移到方法。
Because you want to wrap a transaction around some arbitrary piece of code, you need to override three of the methods of OnMethodBoundaryAspect — namely, OnEntry, OnSuccess, and OnExit. During OnEntry, you create a new TransactionScope, and during OnExit, you dispose of the scope. OnExit is guaranteed to be called. PostSharp will wrap its call in a finally block. Only when the wrapped operation succeeds will you want to invoke the Complete method. That’s why you implement this in the OnSuccess method. You make use of the MethodExecutionTag property to transfer the created TransactionScope from method to method.
While looking at listing 11.4 in isolation, you might find these attributes attractive, but if you compare the code in listing 11.5 to the same aspect in a Decorator (listing 10.15), there’s quite a lot of boilerplate. You need to override multiple methods, apply all kinds of attributes, and pass state from method to method.
This would perhaps be a small price to pay if this would increase maintainability, but there are other, more limiting issues with compile-time weaving that make it unsuitable as a method to apply Volatile Dependencies as Cross-Cutting Concerns.
11.2.2 编译时织入分析
11.2.2 Analysis of compile-time weaving
关于 DI,编译时织入有两个特定的缺点。在本节中,我们将讨论这些限制。虽然还有其他缺点编译时编织,表 11.2中描述的两个捕获了核心问题,使其成为 DI 不受欢迎的方法。
In relationship to DI, compile-time weaving comes with two specific disadvantages. In this section, we’ll discuss these limitations. While there are other downsides to compile-time weaving, the two described in table 11.2 capture the core issue that makes it an undesirable method for DI.
在应用Cross-Cutting Concerns时,您会发现自己经常使用Volatile Dependencies。正如您在第 1 章中了解到的,易失性依赖项是 DI 的焦点。对于Volatile Dependencies,您的默认选择应该是使用Constructor Injection,因为它静态定义了所需依赖项的列表。
When it comes to applying Cross-Cutting Concerns, you’ll find yourself regularly working with Volatile Dependencies. As you learned in chapter 1, Volatile Dependencies are the focal point of DI. With Volatile Dependencies, your default choice should be to use Constructor Injection, because it statically defines the list of required Dependencies.
Unfortunately, it isn’t possible to use Constructor Injection with compile-time weaving aspects. Take a look at the next listing, where we try to use Constructor Injection with a Circuit Breaker aspect.
Listing 11.6 Injecting a Dependency into an aspect using Constructor Injection
[PostSharp.Serialization.PSerializable] ①
public class CircuitBreakerAttribute
: OnMethodBoundaryAspect
{
private readonly ICircuitBreaker breaker;
public CircuitBreakerAttribute( ②
ICircuitBreaker breaker)
{
this.breaker = breaker;
}
public override void OnEntry(
MethodExecutionArgs args)
{
this.breaker.Guard(); ③
}
...
}
This attempt to apply Constructor Injection to this aspect class fails miserably. Remember, you’re defining an attribute that represents separate code, which will be woven into the methods you’ll be working with at compile time. In .NET, attributes can only have primitive types, such as strings and integers, in their constructor.
Even if attributes could have more complex Dependencies, there’d be no way for you to supply an instance of this aspect with an ICircuitBreaker instance, because the aspect is constructed at a completely different time and location from where you’d construct ICircuitBreaker instances. Instances of attributes, like the CircuitBreakerAttribute, are created by the .NET runtime, and there’s no way for you to influence their creation. You have no means of injecting the Dependency into the attribute’s constructor as part of, for instance, the Composition Root:
This issue, however, isn’t limited to working with attributes. Even if the AOP framework uses a mechanism other than attributes, its post-compiler weaves the aspect code into your normal code at compile time and makes it part of the assembly’s code. Your object graphs, on the other hand, are constructed at runtime as part of the Composition Root. These two models don’t mix well. Constructor Injection isn’t possible with compile-time weaving.
Ambient Context and Service Locator are two workarounds for this issue. Both workarounds are, however, hacks with considerable downsides of their own. For the sake of argument, let’s take a look at how to work around the problem using an Ambient Context. The following listing shows the definition of a public static Breaker property in the Circuit Breaker aspect.
As you learned in section 5.3.2, among other things, the Ambient Context causes Temporal Coupling. This means that if you forget to set the Breaker property, the application fails with a NullReferenceException, because the Dependency isn’t optional.
Further, because your only option is to set the property once during application startup, it needs to be defined as static. But this might lead to problems of its own: this could cause the ICircuitBreaker to become a Captive Dependency, as explained in section 8.4.1.
Such a static property causes Interdependent Tests because its value remains in memory when the next test case is executed. It’s therefore necessary to perform Fixture Teardown after each and every test.4 This is something that we must always remember to do — it’s easy to forget. For this reason, compile-time weaving aspects that use an Ambient Context to access Volatile Dependencies aren’t easy to test.
The other workaround is Service Locator, but compared to Ambient Context, it’d only make things worse. Service Locator exhibits the same problems with Interdependent Tests and Temporal Coupling. On top of that, its access to an unbound set of Volatile Dependencies makes it non-obvious as to what its Dependencies are, and it drags along the Service Locator as a redundant Dependency. Because Service Locator is the worse choice, we spare you an example and jump directly into the second disadvantage of compile-time weaving — coupling at compile time.
编译时编织导致编译时耦合
Compile-time weaving causes coupling at compile time
Although compile-time weaving decouples your source code from your aspects, it still causes your compiled code to be tightly coupled with the woven aspects. This is a problem, because Cross-Cutting Concerns often depend on an external system. This problem becomes obvious when you write unit tests, because a unit test must be able to run in isolation. You want to test a class’s logic itself without interdependency with its Volatile Dependencies. You don’t want your unit test crossing process and network boundaries, because communication with a database, filesystem, or other external system will influence the reliability and performance of your tests. In other words, compile-time woven aspects impact Testability.
But even with broadly defined integration tests, compile-time weaving will still cause problems. In an integration test, you test a part of the system in integration with other parts. This lowers the level of isolation, but enables you to find out how individual components work when integrated with others. If you were testing SqlProductRepository, for instance, it wouldn’t make sense to unit test it, because all this Repository does is query the database. You therefore want to test this component’s interaction with the database.
But even in that case, you typically wouldn’t want to have all aspects applied during testing. The use of a [CheckAuthorization] aspect, for instance, might force such a test to go through some sort of login process to verify whether the component can successfully store and retrieve products. It’s important to see whether such an authorization aspect works as expected. Having to run this as part of your test setup for every integration test, unfortunately, makes these tests harder to maintain and, possibly, a lot slower.
A funnier problem manifests itself if you also have caching enabled. In such a case, you could write an automated test with the intent to query the database, but never do so because the test code hits the cache. For this reason, you want to have full control over which aspects are applied to which test and when, in case those aspects are related to Volatile Dependencies. Compile-time weaving complicates this tremendously.
编译时编织不适合用于易失性依赖项
Compile-time weaving is unsuitable for use on Volatile Dependencies
DI 的目的是通过将Seams引入您的应用程序来管理易失性依赖关系。这使您能够将对象图的组合集中在Composition Root中。
The aim of DI is to manage Volatile Dependencies by introducing Seams into your application. This enables you to centralize the composition of your object graphs inside the Composition Root.
这与您在应用编译时织入时获得的结果完全相反:它导致易失性依赖项在编译时耦合到您的代码。这使得无法使用正确的 DI 技术并在应用程序的组合根中安全地组合完整的对象图。正是出于这个原因,我们说编译时织入与 DI 相反——在Volatile Dependencies上使用编译时织入是一种反模式。
This is the complete opposite of what you achieve when applying compile-time weaving: it causes Volatile Dependencies to be coupled to your code at compile time. This makes it impossible to use proper DI techniques and to safely compose complete object graphs in the application’s Composition Root. It’s for this reason that we say that compile-time weaving is the opposite of DI — using compile-time weaving on Volatile Dependencies is an anti-pattern.
Favor applying SOLID principles, falling back to dynamic Interception if that isn’t possible. On that note, we can now leave Pure DI behind in part 3 and move on to read about DI Containers in part 4. There, you’ll learn how DI Containers can fix some of the challenges you might face.
Dynamic Interception is an Aspect-Oriented Programming (AOP) technique that automates the generation of Decorators to be emitted at runtime. Aspects are written as Interceptors, which are injected into a runtime-generated Decorator.
动态拦截具有以下缺点:
失去编译时支持。
方面与工具紧密耦合。
不是普遍适用的。
不解决底层设计问题。
Dynamic Interception exhibits the following disadvantages:
To prevent or delay making design changes like the ones we suggested in chapter 10, dynamic Interception might be a good temporary solution until its time to start making these kinds of improvements.
编译时编织是一种改变编译过程的 AOP 技术。它使用特殊工具通过 IL 操作更改已编译的程序集。这不是将 AOP 应用于Volatile Dependencies的理想方法。
Compile-time weaving is an AOP technique that alters the compilation process. It uses special tools to alter a compiled assembly using IL manipulation. It isn’t a desirable method of applying AOP to Volatile Dependencies.
关于 DI,编译时织入存在以下问题:
编译时编织方面对 DI 不友好。
编译时织入导致编译时紧耦合。
In relation to DI, compile-time weaving exhibits the following problems:
Compile-time weaving aspects are DI-unfriendly.
Compile-time weaving causes tight coupling at compile time.
赞成应用SOLID原则,如果不可能则回退到动态拦截。
Favor applying SOLID principles, falling back to dynamic Interception if that isn’t possible.
第 4 部分
DI 容器
Part 4
DI Containers
吨本书的前几部分介绍了共同定义 DI 的各种原则和模式。正如第 3 章所解释的那样,DI 容器是一个可选工具,您可以使用它来实现许多通用基础设施,如果您使用Pure DI ,您将不得不实现这些基础设施。
The previous parts of the book have been about the various principles and patterns that together define DI. As chapter 3 explained, a DI Container is an optional tool that you can use to implement a lot of the general-purpose infrastructure that you would otherwise have to implement if you were using Pure DI.
在整本书中,我们一直在讨论容器不可知论,这意味着我们只教你Pure DI。不要将此解释为Pure DI本身的推荐;相反,我们希望您看到最纯粹形式的 DI,不受任何特定容器 API 的污染。
Throughout the book, we’ve kept the discussion container agnostic, which means we’ve only taught you Pure DI. Don’t interpret this as a recommendation of Pure DI per se; rather, we want you to see DI in its purest form, untainted by any particular container’s API.
Many excellent DI Containers are available for the .NET platform. In chapter 12, we’ll discuss when you should use one of these containers and when you should stick with Pure DI. The remaining chapters in part 4 cover a selection of three free and open source DI Containers. In each chapter, we provide detailed coverage of that particular container’s API as it relates to the dimensions covered in part 3, as well as various other issues that traditionally cause beginners grief. The containers covered are Autofac (chapter 13), Simple Injector (chapter 14), and Microsoft.Extensions.DependencyInjection (chapter 15).
Given unlimited space and time, we wanted to include all containers, but alas, that wasn’t possible. We excluded all but one of the containers covered in the first edition. Those excluded include Castle Windsor, StructureMap, String.NET, Unity, and MEF. For more information on those, grab your copy of the first edition (you get it free with this edition). Also, we considered, but didn’t include, Ninject, which is one of the more popular DI Containers. At the time of writing, there is no .NET Core–compatible version available, which was a criterion for inclusion.
All the containers described are open source projects with fast release cycles. Before we discuss the containers in this part, chapter 12 goes into more detail about what a container is, what it helps you with, and how to decide when to use a DI Container or stick with using Pure DI.
Because of its market share, we simply couldn’t exclude Autofac, even though we covered it in the first edition. Autofac is the most popular DI Container for .NET. Chapter 13 is dedicated to it. And although we included Microsoft.Extensions.DependencyInjection (MS.DI), we’re skeptical of it, because it’s limited in functionality. However, we felt obliged to cover it, because many developers are inclined to use the built-in tooling first before switching to third-party tooling. Chapter 15 will explain what MS.DI can and can’t do.
Each chapter follows a common template. This may give you a certain sense of déjà vu as you read the same sentence for the third time. We consider it an advantage, because it should make it easy for you to quickly find similar sections across different chapters if you want to compare how a specific feature is addressed across containers.
These chapters are meant as inspiration. If you have yet to pick a favorite DI Container, you can read through all three chapters to compare them, but you can also just read the one that particularly interests you. The information presented in part 4 was accurate at the time of writing, but always be sure to consult more up-to-date sources as well.
12
DI容器介绍
12
DI Container introduction
在这一章当中
In this chapter
使用配置文件启用后期绑定
Using configuration files to enable late binding
使用配置即代码在DI 容器中显式注册组件
Explicitly registering components in a DI Container with Configuration as Code
在具有自动注册的DI 容器中应用约定优于配置
Applying Convention over Configuration in a DI Container with Auto-Registration
选择应用纯 DI还是使用DI 容器
Choosing between applying Pure DI or using a DI Container
When I (Mark) was a kid, my mother and I would occasionally make ice cream. This didn’t happen too often because it required work, and it was hard to get right. Real ice cream is based on a crème anglaise, which is a light custard made from sugar, egg yolks, and milk or cream. If heated too much, this mixture curdles. Even if you manage to avoid this, the next phase presents more problems. Left alone in the freezer, the cream mixture crystallizes, so you have to stir it at regular intervals until it becomes so stiff that this is no longer possible. Only then will you have a good, homemade ice cream. Although this is a slow and labor-intensive process, if you want to — and you have the necessary ingredients and equipment — you can use this technique to make ice cream.
Today, some 35 years later, my mother-in-law makes ice cream with a frequency unmatched by myself and my mother at much younger ages — not because she loves making ice cream, but because she uses technology to help her. The technique is still the same, but instead of regularly taking out the ice cream from the freezer and stirring it, she uses an electric ice cream maker to do the work for her (see figure 12.1).
Figure 12.1 An Italian ice cream maker. As with making ice cream, with better technology, you can accomplish programming tasks more easily and quickly.
DI 首先是一种技术,但您可以使用技术使事情变得更容易。在第 3 部分中,我们将 DI 描述为一种技术。在第 4 部分中,我们将了解可用于支持 DI 技术的技术。我们称这种技术为 DI 容器。
DI is first and foremost a technique, but you can use technology to make things easier. In part 3, we described DI as a technique. Here, in part 4, we take a look at the technology that can be used to support the DI technique. We call this technology DI Containers.
在本章中,我们将把DI 容器视为一个概念——它们如何融入 DI 的整体主题——以及有关它们的使用的一些模式和实践。我们还将在此过程中查看一些示例。
In this chapter, we’ll look at DI Containers as a concept — how they fit into the overall topic of DI — as well as some patterns and practices concerning their usage. We’ll also look at some examples along the way.
This chapter begins with a general introduction to DI Containers, including a description of a concept called Auto-Wiring, followed by a section on various configuration options. You can read about each of these configuration options in isolation, but we think it’d be beneficial to at least read about Configuration as Code before you read about Auto-Registration.
最后一段不一样。它重点介绍了使用DI 容器的优点和缺点,并帮助您确定使用DI 容器是否对您和您的应用程序有益。我们认为这是每个人都应该阅读的重要部分,无论他们是否有使用 DI 和DI 容器的经验。本节可以单独阅读,但最好先阅读配置即代码和自动注册部分。
The last section is different. It focuses on the advantages and disadvantages of using DI Containers and helps you decide whether the use of a DI Container is beneficial to you and your applications. We think this an important part that everyone should read, regardless of their experience with DI and DI Containers. This section can be read in isolation, although it would be beneficial to read the sections on Configuration as Code and Auto-Registration first.
The purpose of this chapter is to give you a good understanding of what a DI Container is and how it fits in with the rest of the patterns and principles in this book. In a sense, you can view this chapter as an introduction to part 4 of the book. Here, we’ll talk about DI Containers in general, whereas in the following chapters, we’ll talk about specific containers and their APIs.
12.1 引入DI 容器
12.1 Introducing DI Containers
DI 容器是一个软件库,可以自动执行对象组合、生命周期管理和拦截中涉及的许多任务。尽管可以使用Pure DI编写所有必需的基础结构代码,但它不会为应用程序增加太多价值。另一方面,组合对象的任务具有一般性,可以一劳永逸地解决;这就是所谓的通用子域. 1 鉴于此,使用通用库是有意义的。它与实现日志记录或数据访问没有太大区别;记录应用程序数据是一种可以通过通用日志记录库解决的问题。组合对象图也是如此。
A DI Container is a software library that can automate many of the tasks involved in Object Composition, Lifetime Management, and Interception. Although it’s possible to write all the required infrastructure code with Pure DI, it doesn’t add much value to an application. On the other hand, the task of composing objects is of a general nature and can be resolved once and for all; this is what’s known as a Generic Subdomain.1 Given this, using a general-purpose library can make sense. It’s not much different than implementing logging or data access; logging application data is the kind of problem that can be addressed by a general-purpose logging library. The same is true for composing object graphs.
In this section, we’ll discuss how DI Containers compose object graphs. We’ll also show you some examples to give you a general sense of what using a container and an implementation might look like.
12.1.1 探索容器的 Resolve API
12.1.1 Exploring containers’ Resolve API
DI 容器是一个软件库,就像任何其他软件库一样。它公开了一个 API,您可以使用它来组合对象,并且组合对象图是一个单一的方法调用。DI 容器还要求您在组合对象之前配置它们。我们将在 12.2 节中重新讨论它。
A DI Container is a software library like any other software library. It exposes an API that you can use to compose objects, and composing an object graph is a single method call. DI Containers also require you to configure them prior to composing objects. We’ll revisit that in section 12.2.
在这里,我们将向您展示一些示例,说明DI 容器如何解析对象图。作为本节中的示例,我们将同时使用 Autofac 和 Simple Injector到 ASP.NET Core MVC 应用程序。有关如何编写 ASP.NET Core MVC 应用程序的更多详细信息,请参阅第 7.3 节。
Here, we’ll show you some examples of how DI Containers can resolve object graphs. As examples in this section, we’ll use both Autofac and Simple Injector applied to an ASP.NET Core MVC application. Refer to section 7.3 for more detailed information about how to compose ASP.NET Core MVC applications.
You can use a DI Container to resolve controller instances. This functionality can be implemented with all three DI Containers covered in the following chapters, but we’ll show only a couple of examples here.
Autofac is a DI Container with a fairly pattern-conforming API. Assuming you already have an Autofac container instance, you can resolve a controller by supplying the requested type:
var controller = (HomeController)container.Resolve(typeof(HomeController));
You’ll pass typeof(HomeController) to the Resolve method and get back an instance of the requested type, fully populated with all the appropriate Dependencies. The Resolve method is weakly typed and returns an instance of System.Object; this means you’ll need to cast it to something more specific, as the example shows.
Many of the DI Containers have APIs that are similar to Autofac’s. The corresponding code for Simple Injector looks nearly identical to Autofac’s, even though instances are resolved using the SimpleInjector.Container class. With Simple Injector, the previous code would look like this:
The only real difference is that the Resolve method is called GetInstance. You can extract a general shape of a DI Container from these examples.
使用DI 容器解析对象图
Resolving object graphs with DI Containers
DI 容器是解析和管理对象图的引擎。尽管DI 容器不仅仅是解析对象,但这是任何容器 API 的核心部分。前面的例子表明容器有一个用于此目的的弱类型方法。随着名称和签名的变化,该方法如下所示:
A DI Container is an engine that resolves and manages object graphs. Although there’s more to a DI Container than resolving objects, this is a central part of any container’s API. The previous examples show that containers have a weakly typed method for that purpose. With variations in names and signatures, that method looks like this:
As the previous examples demonstrate, because the returned instance is typed as System.Object, you often need to cast the return value to the expected type before using it. Many DI Containers also offer a generic version for those cases where you know which type to request at compile time. They often look like this:
Instead of supplying a Type method argument, such an overload takes a type parameter (T) that indicates the requested type. The method returns an instance of T. Most containers throw an exception if they can’t resolve the requested type.
If we view the Resolve method in isolation, it almost looks like magic. From the compiler’s perspective, it’s possible to ask it to resolve instances of arbitrary types. How does the container know how to compose the requested type, including all Dependencies? It doesn’t; you’ll have to tell it first. You do so using a configuration that maps Abstractions to concrete types. We’ll return to this topic in section 12.2.
If a container has insufficient configuration to fully compose a requested type, it’ll normally throw a descriptive exception. As an example, consider the following HomeController we first discussed in listing 3.4. As you might remember, it contains a Dependency of type IProductService:
public class HomeController : Controller
{
private readonly IProductService productService;
public HomeController(IProductService productService)
{
this.productService = productService;
}
...
}
在配置不完整的情况下,Simple Injector 具有如下示例性异常消息:
With an incomplete configuration, Simple Injector has exemplary exception messages like this one:
The constructor of type HomeController contains the parameter with name 'productService' and type IProductService, which isn’t registered. Please ensure IProductService is registered or change the constructor of HomeController.
In the previous example, you can see that Simple Injector can’t resolve HomeController, because it contains a constructor argument of type IProductService, but Simple Injector wasn’t told which implementation to return when IProductService was requested. If the container is correctly configured, it can resolve even complex object graphs from the requested type. If something is missing from the configuration, the container can provide detailed information about what’s missing. In the next section, we’ll take a closer look at how this is done.
12.1.2 自动接线
12.1.2 Auto-Wiring
DI 容器在编译到所有类中的静态信息上茁壮成长。使用反射,他们可以分析请求的类并找出需要哪些依赖项。
DI Containers thrive on the static information compiled into all classes. Using reflection, they can analyze the requested class and figure out which Dependencies are needed.
如第 4.2 节所述,构造函数注入是应用 DI 的首选方式,因此,所有DI 容器天生就理解构造函数注入。具体来说,他们通过将自己的配置与从类的类型信息中提取的信息相结合来组成对象图。这称为自动接线。
As explained in section 4.2, Constructor Injection is the preferred way of applying DI and, because of this, all DI Containers inherently understand Constructor Injection. Specifically, they compose object graphs by combining their own configuration with the information extracted from the classes’ type information. This is called Auto-Wiring.
Most DI Containers also understand Property Injection, although some require you to explicitly enable it. Considering the downsides of Property Injection (as explained in section 4.4), this is a good thing. Figure 12.2 describes the general algorithm most DI Containers follow to Auto-Wire an object graph.
Figure 12.2 Simplified workflow for Auto-Wiring. A DI Container uses its configuration to find the appropriate concrete class that matches the requested type. It then uses reflection to examine the class’s constructor.
As shown, a DI Container finds the concrete type for a requested Abstraction. If the constructor of the concrete type requires arguments, a recursive process starts where the DI Container repeats the process for each argument type until all constructor arguments are satisfied. When this is complete, the container constructs the concrete type while injecting the recursively resolved Dependencies.
In section 12.2, we’ll take a closer look at how containers can be configured. For now, the most important thing to understand is that at the core of the configuration is a list of mappings between Abstractions and their represented concrete classes. That sounds a bit theoretical, so we think an example will be helpful.
12.1.3 示例:实现一个支持自动装配的简单DI 容器
12.1.3 Example: Implementing a simplistic DI Container that supports Auto-Wiring
To demonstrate how Auto-Wiring works, and to show that there’s nothing magical about DI Containers, let’s look at a simplistic DI Container implementation that’s able to build complex object graphs using Auto-Wiring.
Listing 12.1 shows this simplistic DI Container implementation. It doesn’t support Lifetime Management, Interception, or many other important features. The only supported feature is Auto-Wiring.
Listing 12.1 A simplistic DI Container that supports Auto-Wiring
public class AutoWireContainer
{
Dictionary<Type, Func<object>> registrations = ①
new Dictionary<Type, Func<object>>();
public void Register(
Type serviceType, Type componentType) ② { ② this.registrations[serviceType] = ② () => this.CreateNew(componentType); ② } ② public void Register( ③ Type serviceType, Func<object> factory) ③ { ③ this.registrations[serviceType] = factory; ③ } ③ public object Resolve(Type type) ④ { ④ if(this.registrations.ContainsKey(type)) ④ { ④ return this.registrations[type](); ④ } ④ ④ throw new InvalidOperationException( ④ "No registration for " + type); ④
}
private object CreateNew(Type componentType) ⑤ { ⑤ var ctor = ⑤ componentType.GetConstructors()[0]; ⑤ ⑤ var dependencies = ⑤ from p in ctor.GetParameters() ⑤ select this.Resolve(p.ParameterType); ⑤ ⑤ return Activator.CreateInstance( ⑤ componentType, dependencies.ToArray()); ⑤
}
}
The AutoWireContainer contains a set of registrations. A registration is a mapping between an Abstraction (the service type) and a component type. The Abstraction is presented as the dictionary’s key, whereas its value is a Func<object> delegate that allows constructing a new instance of a component that implements the Abstraction. The Register method registers a new registration by telling the container which component should be created for a given service type. You only specify which component to create, not how.
The Register method adds the mapping for the service type to the registrations dictionary. Optionally, the Register method can supply the container with a Func<T> delegate directly. This bypasses its Auto-Wiring abilities. It will call the supplied delegate instead.
The Resolve methods allows resolving a complete object graph. It gets the Func<T> from the registrations dictionary for the requested serviceType, invokes it, and returns its value. In case there’s no registration for the requested type, Resolve throws an exception. And finally, CreateNew creates a new instance of a component by iterating over the component’s constructor parameters and calling back into the container recursively. It does so by calling Resolve for each parameter, while supplying the parameter’s Type. When all the type’s Dependencies are resolved in this way, it constructs the type itself by using reflection (using the System.Activator class).
An AutoWireContainer instance can be configured to compose arbitrary object graphs. Back in chapter 3 in listing 3.13, you created a HomeController using Pure DI. The next listing repeats that listing from chapter 3. We’ll use that as an example to demonstrate the Auto-Wiring capabilities previously defined in AutoWireContainer.
Instead of composing this object graph by hand, as done in the previous listing, you can use the AutoWireContainer to register the five required components. To do this, you must map these five components to their appropriate Abstraction. Table 12.1 lists these mappings.
清单 12.3展示了如何使用AutoWireContainer的Register方法添加表 12.1中指定的所需映射。请注意,此清单使用Configuration as Code。我们将在 12.2.2 节中讨论配置即代码。
Listing 12.3 shows how you can use the AutoWireContainer’s Register methods to add the required mappings specified in table 12.1. Note that this listing uses Configuration as Code. We’ll discuss Configuration as Code in section 12.2.2.
Listing 12.3 Using AutoWireContainer to register HomeController
var container = new AutoWireContainer(); ① container.Register( ② typeof(IUserContext), ② typeof(AspNetUserContextAdapter)); ② ② container.Register( ② typeof(IProductRepository), ② typeof(SqlProductRepository)); ② ② container.Register( ② typeof(IProductService), ② typeof(ProductService)); ② container.Register( ③ typeof(HomeController), ③ typeof(HomeController)); ③ container.Register( ④ typeof(CommerceContext), ④ () => new CommerceContext(connectionString)); ④
You might find the mapping for HomeController in table 12.1 and listing 12.3 confusing, because it maps to itself instead of mapping to an Abstraction. This is a common practice, however, especially when dealing with types that are at the top of the object graph, such as MVC controllers.
You saw something similar in listings 4.4, 7.8, and 8.3, where you created a new HomeController instance when a HomeController type was requested. The main difference between those listings and listing 12.3 is that the latter uses a DI Container instead of Pure DI.
Listing 12.3 effectively registered all components required for the composition of an object graph of HomeController. You can now use the configured AutoWireContainer to create a new HomeController.
When the AutoWireContainer’s Resolve method is called to request a new HomeController type, the container will call itself recursively until it has resolved all of its required Dependencies. After this, a new HomeController instance is created, while supplying the resolved Dependencies to its constructor. Figure 12.3 shows the recursive process, using a somewhat unconventional representation to visualize recursive calls. The container instance is spread out over four separate vertical time lines. Because there are multiple levels of recursive calls, folding them into one single line, as is the norm with UML sequence diagrams, would be quite confusing.
图 12.3 Composition RootHomeController从 the 请求a container,它递归地回调到自身以请求HomeController的Dependencies。
Figure 12.3 The Composition Root requests a HomeController from the container, which recursively calls back into itself to request HomeController’s Dependencies.
当DI 容器收到对 a 的请求时HomeController,它要做的第一件事是在其配置中查找类型。HomeController是一个具体类,您将其映射到自身。然后容器使用反射来检查HomeController具有以下签名的唯一构造函数:
When the DI Container receives a request for a HomeController, the first thing it’ll do is look up the type in its configuration. HomeController is a concrete class, which you mapped to itself. The container then uses reflection to inspect HomeController’s one and only constructor with the following signature:
public HomeController(IProductService productService)
Because this constructor isn’t a parameterless constructor, it needs to repeat the process for the IProductService constructor argument when following the general flowchart from figure 12.2. The container looks up IProductService in its configuration and finds that it maps to the concrete ProductService class. The single public constructor for ProductService has this signature:
public ProductService(
IProductRepository repository,
IUserContext userContext)
That’s still not a parameterless constructor, and now there are two constructor arguments to deal with. The container takes care of each in order, so it starts with the IProductRepository interface that, according to the configuration, maps to SqlProductRepository. That SqlProductRepository has a public constructor with this signature:
public SqlProductRepository(CommerceContext context)
That’s again not a parameterless constructor, so the container needs to resolve CommerceContext to satisfy SqlProductRepository’s constructor. CommerceContext, however, is registered in listing 12.3 using the following delegate:
Now that the container has the appropriate value for CommerceContext, it can invoke the SqlProductRepository constructor. It has now successfully handled the Repository parameter for the ProductService constructor, but it’ll need to hold on to that value for a while longer; it also needs to take care of ProductService’s userContext constructor parameter. According to the configuration, IUserContext maps to the concrete AspNetUserContextAdapter class, which has this public constructor:
Because AspNetUserContextAdapter contains a parameterless constructor, it can be created without having to resolve any Dependencies. It can now pass the new AspNetUserContextAdapter instance to the ProductService constructor. Together with the SqlProductRepository from before, it now fulfills the ProductService constructor and invokes it via reflection. Finally, it passes the newly created ProductService instance to the HomeController constructor and returns the HomeController instance. Figure 12.4 shows how the general workflow presented in figure 12.2 maps to the AutoWireContainer from listing 12.1.
Figure 12.4 Simplified workflow for Auto-Wiring mapped to the code from listing 12.1. The registrations dictionary is queried for the concrete type, its constructor parameters get resolved, and the concrete type is created using its resolved Dependencies.
The advantage of using a DI Container’s Auto-Wiring capabilities as shown in listing 12.3 rather than using Pure DI as shown in listing 12.2 is that with Pure DI, any change to a component’s constructor needs to be reflected in the Composition Root. Auto-Wiring, on the other hand, makes the Composition Root more resilient to such changes.
For example, let’s say you need to add a CommerceContextDependency to AspNetUserContextAdapter in order for it to query the database. The following listing shows the change that needs to be made to the Composition Root when you apply Pure DI.
Listing 12.5Composition Root for the changed AspNetUserContextAdapter
new HomeController(
new ProductService(
new SqlProductRepository(
new CommerceContext(connectionString)),
new AspNetUserContextAdapter(
new CommerceContext(connectionString)))); ①
With Auto-Wiring, on the other hand, no changes to the Composition Root are required in this case. AspNetUserContextAdapter is Auto-Wired, and because its new CommerceContextDependency was already registered, the container will be able to satisfy the new constructor argument and will happily construct a new AspNetUserContextAdapter.
This is how Auto-Wiring works, although DI Containers also need to take care of Lifetime Management and, perhaps, address Property Injection as well as other, more specialized, creational requirements.
The salient point is that Constructor Injection statically advertises the Dependency requirements of a class, and DI Containers use that information to Auto-Wire complex object graphs. A container must be configured before it can compose object graphs. Registration of components can be done in various ways.
12.2 配置DI 容器
12.2 Configuring DI Containers
虽然该Resolve方法是大部分操作发生的地方,但您应该期望将大部分时间花在DI Container的配置 API 上。毕竟,解析对象图是一个单一的方法调用。
Although the Resolve method is where most of the action happens, you should expect to spend most of your time with a DI Container’s configuration API. Resolving object graphs is, after all, a single method call.
Figure 12.5 The most common ways to configure a DI Container shown against dimensions of explicitness and the degree of binding
DI 容器倾向于支持两个或三个常见的配置选项,如图 12.5所示。有些不支持配置文件,有些也不支持自动注册,而配置即代码支持无处不在。大多数允许您在同一个应用程序中混合使用多种方法。第 12.2.4 节讨论了为什么要使用混合方法。
DI Containers tend to support two or three of the common configuration options shown in figure 12.5. Some don’t support configuration files, and others also lack support for Auto-Registration, whereas Configuration as Code support is ubiquitous. Most allow you to mix several approaches in the same application. Section 12.2.4 discusses why you’d want to use a mixed approach.
These three configuration options have different characteristics that make them useful in different situations. Both configuration files and Configuration as Code tend to be explicit, because they require you to register each component individually. Auto-Registration, on the other hand, is more implicit because it uses conventions to register a set of components by a single rule.
当您使用Configuration as Code时,您将容器配置编译到程序集中,而基于文件的配置使您能够支持后期绑定,您可以在其中更改配置而无需重新编译应用程序。在该维度中,自动注册处于中间位置,因为您可以要求它扫描编译时已知的单个程序集,或者扫描编译时可能未知的预定义文件夹中的所有程序集。表 12.2列出了每个选项的优点和缺点。
When you use Configuration as Code, you compile the container configuration into an assembly, whereas file-based configuration enables you to support late binding, where you can change the configuration without recompiling the application. In that dimension, Auto-Registration falls somewhere in the middle, because you can ask it to scan a single assembly known at compile time or, alternatively, to scan all assemblies in a predefined folder that might be unknown at compile time. Table 12.2 lists the advantages and disadvantages of each option.
Historically, DI Containers started out with configuration files, which also explains why the older libraries still support this. But this feature has been downplayed in favor of more conventional approaches. That’s why more recently developed DI Containers, such as Simple Injector and Microsoft.Extensions.DependencyInjection, don’t have any built-in support for file-based configuration.
Although Auto-Registration is the most modern option, it isn’t the most obvious place to start. Because of its implicitness, it may seem more abstract than the more explicit options, so instead, we’ll cover each option in historical order, starting with configuration files.
12.2.1 使用配置文件配置容器
12.2.1 Configuring containers with configuration files
当DI 容器在 2000 年代初首次出现时,它们都使用 XML 作为配置机制——当时大多数东西都是这样做的。后来将 XML 作为配置机制的经验表明,这很少是最佳选择。
When DI Containers first appeared back in the early 2000s, they all used XML as a configuration mechanism — most things did back then. Experience with XML as a configuration mechanism later revealed that this is rarely the best option.
XML 往往冗长而脆弱。当您在 XML 中配置DI 容器时,您会识别各种类和接口,但如果您拼错某些内容,则没有编译器支持来警告您。即使类名正确,也不能保证所需的程序集将位于应用程序的探测路径中。
XML tends to be verbose and brittle. When you configure a DI Container in XML, you identify various classes and interfaces, but you have no compiler support to warn you if you misspell something. Even if the class names are correct, there’s no guarantee that the required assembly is going to be in the application’s probing path.
To add insult to injury, the expressiveness of XML is limited compared to that of plain code. This sometimes makes it hard or impossible to express certain configurations in a configuration file that are otherwise trivial to express in code. In listing 12.3, for instance, you registered the CommerceContext using a lambda expression. Such a lambda expression can be expressed in neither XML nor JSON.
The advantage of configuration files, on the other hand, is that you can change the behavior of the application without recompilation. This is valuable if you develop software that ships to thousands of customers, because it gives them a way to customize the application. But if you write an internal application or a website where you control the deployment environment, it’s often easier to recompile and redeploy the application when you need to change the behavior.
DI 容器通常通过将其指向特定的配置文件来配置文件。以下示例以 Autofac 为例。
A DI Container is often configured with files by pointing it to a particular configuration file. The following example uses Autofac as an example.
In this example, you’ll configure the same classes as in section 12.1.3. A large part of the task is to apply the configuration outlined in table 12.1, but you must also supply a similar configuration to support composition of the HomeController class. The following listing shows the configuration necessary to get the application up and running.
In this example, if you don’t specify an assembly-qualified type name in a type or interface reference, defaultAssembly will be assumed to be the default assembly. For a simple mapping, full type names must be used, including namespace and assembly name. Because AspNetUserContextAdapter excluded the name of the assembly, Autofac looks for it in the Commerce.Web assembly, which you defined as the defaultAssembly.
As you can see from even this simple code listing, JSON configuration tends to be quite verbose. Simple mappings like the one from the IUserContext interface to the AspNetUserContextAdapter class require quite a lot of text in the form of brackets and fully qualified type names.
As you may recall, CommerceContext takes a connection string as input, so you need to specify how the value of this string is found. By adding parameters to a mapping, you can specify values by their parameter name — in this case, connectionString. Loading the configuration into the container is done with the following code.
Listing 12.7 Reading configuration files using Autofac
var builder = new Autofac.ContainerBuilder(); ① IConfigurationRoot configuration = ② new ConfigurationBuilder() ② .AddJsonFile("autofac.json") ② .Build(); ② builder.RegisterModule( ③ new Autofac.Configuration.ConfigurationModule( ③ configuration)); ③
Autofac is the only DI Container included in this book that supports configuration files, but there are other DI Containers not covered here that continue to support configuration files. The exact schema is different for each container, but the overall structure tends to be similar, because you need to map an Abstraction to an implementation.
Don’t let the absence of support for handling configuration files influence your choice of a DI Container too much. As described previously, only true late-bound components should be defined in configuration files, which will unlikely be more than a handful. Even with absence of support from your container, types can be loaded from configuration files in a few simple statements, as shown in listing 1.2.
由于冗长和脆弱的缺点,您应该更喜欢其他配置容器的方法。Configuration as Code在粒度和概念上类似于配置文件,但显然使用代码而不是配置文件。
Because of the disadvantages of verbosity and brittleness, you should prefer the other alternatives for configuring containers. Configuration as Code is similar to configuration files in granularity and concept, but obviously uses code instead of configuration files.
12.2.2 使用配置即代码配置容器
12.2.2 Configuring containers using Configuration as Code
也许编写应用程序的最简单方法是对对象图的构造进行硬编码。这似乎违背了 DI 的整体精神,因为它决定了在编译时应该用于所有抽象的具体实现。但如果在Composition Root中完成,它只会违反表 1.1 中列出的好处之一,即后期绑定。
Perhaps the easiest way to compose an application is to hard code the construction of object graphs. This may seem to go against the whole spirit of DI, because it determines the concrete implementations that should be used for all Abstractions at compile time. But if done in a Composition Root, it only violates one of the benefits listed in table 1.1, namely, late binding.
The benefit of late binding is lost if Dependencies are hard-coded, but, as we mentioned in chapter 1, this may not be relevant for all types of applications. If your application is deployed in a limited number of instances in a controlled environment, it can be easier to recompile and redeploy the application if you need to replace modules:
I often think that people are over-eager to define configuration files. Often a programming language makes a straightforward and powerful configuration mechanism.4
马丁福勒
Martin Fowler
当您使用Configuration as Code时,您明确声明了与使用配置文件时相同的离散映射——只是您使用代码而不是 XML 或 JSON。
When you use Configuration as Code, you explicitly state the same discrete mappings as when you use configuration files — only you use code instead of XML or JSON.
All modern DI Containers fully support Configuration as Code as the successor to configuration files; in fact, most of them present this as the default mechanism, with configuration files as an optional feature. As stated previously, some don’t even offer support for configuration files at all. The API exposed to support Configuration as Code differs from DI Container to DI Container, but the overall goal is still to define discrete mappings between Abstractions and concrete types.
Let’s take a look how to configure the e-commerce application using Configuration as Code with Microsoft.Extensions.DependencyInjection. For this, we’ll use an example that configures the sample e-commerce application with code.
In section 12.2.1, you saw how to configure the sample e-commerce application with configuration files using Autofac. We could also demonstrate Configuration as Code with Autofac, but, to make this chapter a bit more interesting, we’ll instead use Microsoft.Extensions.DependencyInjection in this example. Using Microsoft’s configuration API, you can express the configuration from listing 12.6 more compactly, as shown here.
Listing 12.8 Configuring Microsoft.Extensions.DependencyInjection with code
var services = new ServiceCollection(); ① services.AddSingleton< ② IUserContext, ② AspNetUserContextAdapter>(); ②
services.AddTransient<
IProductRepository,
SqlProductRepository>();
services.AddTransient<
IProductService,
ProductService>();
services.AddTransient<HomeController>(); ③ services.AddScoped<CommerceContext>( ④ p => new CommerceContext(connectionString)); ④
ServiceCollection是 Microsoft 的 Autofac 的等价物ContainerBuilder,它定义了抽象和实现之间的映射。、和方法用于在抽象和具体类型之间为其特定的Lifestyle添加自动连线映射。这些方法是通用的,这会产生更简洁的代码,并带来一些额外的编译时检查的额外好处。如果具体类型映射到自身,而不是将抽象映射到具体类型,则有一个方便的重载,它只将具体类型作为泛型类型参数。并且,就像清单 12.1的例子一样AddTransientAddScopedAddSingletonAutoWireContainer,此DI 容器的 API包含允许将抽象映射到Func<T>委托的重载。
ServiceCollection is Microsoft’s equivalent to Autofac’s ContainerBuilder, which defines the mappings between Abstractions and implementations. The AddTransient, AddScoped, and AddSingleton methods are used to add Auto-Wired mappings between Abstractions and concrete types for their specific Lifestyle. These methods are generic, which results in more condensed code with the additional benefit of getting some extra compile-time checking. In case a concrete type maps to itself, instead of having an Abstraction mapping to a concrete type, there’s a convenient overload that just takes in the concrete type as a generic type argument. And, just as with the AutoWireContainer example of listing 12.1, the API of this DI Container contains an overload that allows mapping an Abstraction to a Func<T> delegate.
In listing 12.8, we took the liberty of demonstrating the registration of components using the three common lifestyles: Singleton, Transient, and Scoped. The following chapters show how to configure lifestyles for each container in more detail.
将此代码与清单 12.6进行比较,注意它的紧凑程度——尽管它做的事情完全相同。像 from IProductServiceto这样的简单映射ProductService是用单个方法调用表示的。
Compare this code with listing 12.6, and notice how much more compact it is — even though it does the exact same thing. A simple mapping like the one from IProductService to ProductService is expressed with a single method call.
配置即代码不仅比配置文件中表达的配置更紧凑,而且还享有编译器支持。清单 12.8中使用的类型参数表示编译器检查的实际类型。泛型更进一步,因为使用泛型类型约束(例如 Microsoft 的 API 应用)允许编译器检查提供的具体类型是否与抽象相匹配。如果无法进行转换,则代码将无法编译。
Not only is Configuration as Code much more compact than configurations expressed in a configuration file, it also enjoys compiler support. The type arguments used in listing 12.8 represent real types that the compiler checks. Generics go even a step further, because the use of generic type constraints such as Microsoft’s API applies allows the compiler to check whether the supplied concrete type matches the Abstraction. If a conversion isn’t possible, the code won’t compile.
Although Configuration as Code is safe and easy to use, it still requires more maintenance than you might like. Every time you add a new type to an application, you must also remember to register it — and many registrations end up being similar. Auto-Registration addresses this issue.
12.2.3 使用自动注册按照惯例配置容器
12.2.3 Configuring containers by convention using Auto-Registration
Considering the registrations of listing 12.8, it might be completely fine to have these few lines of code in your project. When a project grows, however, so will the amount of registrations required to set up the DI Container. In time, you’re likely to see many similar registrations appear. They’ll typically follow a common pattern. The following listing shows how these registrations can start to look somewhat repetitive.
这样重复写注册码就违反了DRY原则。它也似乎是一段无用的基础架构代码,不会为应用程序增加太多价值。如果您可以自动注册组件,您可以节省时间并减少错误,假设这些组件遵循某种约定。许多DI 容器提供自动注册功能,让您可以引入自己的约定并应用Convention over Configuration。
Repeatedly writing registration code like that violates the DRY principle. It also seems like an unproductive piece of infrastructure code that doesn’t add much value to the application. You can save time and make fewer errors if you can automate the registration of components, assuming those components follow some sort of convention. Many DI Containers provide Auto-Registration capabilities that let you introduce your own conventions and apply Convention over Configuration.
In reality, you may need to combine Auto-Registration with Configuration as Code or configuration files, because you may not be able to fit every single component into a meaningful convention. But the more you can move your code base towards conventions, the more maintainable it will be.
Autofac supports Auto-Registration, but we thought it would be more interesting to use yet another DI Container to configure the sample e-commerce application using conventions. Because we like to restrain the examples to the DI Containers discussed in this book, and because Microsoft.Extensions.DependencyInjection doesn’t have any Auto-Registration facilities, we’ll use Simple Injector to illustrate this concept.
Looking back at listing 12.9, you’ll likely agree that the registrations of the various data access components are repetitive. Can we express some sort of convention around them? All five concrete Repository types of listing 12.9 share some characteristics:
它们都在同一个程序集中定义。
They’re all defined in the same assembly.
每个具体类都有一个以Repository结尾的名称。
Each concrete class has a name that ends with Repository.
每个都实现一个接口。
Each implements a single interface.
似乎适当的约定会通过扫描有问题的程序集并注册所有符合约定的类来表达这些相似性。即使 Simple Injector 确实支持Auto-Registration,它的Auto-Registration API 侧重于注册共享相同接口的类型组。它的 API 本身不允许您表达这种约定,因为没有单一的接口来描述这组存储库。
It seems that an appropriate convention would express these similarities by scanning the assembly in question and registering all classes that match the convention. Even though Simple Injector does support Auto-Registration, its Auto-Registration API focuses around the registration of groups of types that share the same interface. Its API, by itself, doesn’t allow you to express this convention, because there’s no single interface that describes this group of repositories.
起初,这种遗漏可能看起来相当尴尬,但在 .NET 的反射 API 之上定义自定义 LINQ 查询通常很容易编写,提供更多的灵活性,并且使您不必学习另一个 API — 假设您熟悉 LINQ和 .NET 的反射 API。以下清单显示了使用 LINQ 查询的此类约定。
At first, this omission might seem rather awkward, but defining a custom LINQ query on top of .NET’s reflection API is typically easy to write, provides more flexibility, and prevents you from having to learn another API — assuming you’re familiar with LINQ and .NET’s reflection API. The following listing shows such a convention using a LINQ query.
Listing 12.10 Convention for scanning repositories using Simple Injector
var assembly = ① typeof(SqlProductRepository).Assembly; ① var repositoryTypes = ② from type in assembly.GetTypes() ② where !type.Abstract ② where type.Name.EndsWith("Repository") ② select type; ② foreach (Type type in repositoryTypes) ③ { ③ container.Register( ③ type.GetInterfaces().Single(), type); ③ } ③
Each of the classes that make it through the where filters during iteration should be registered against their interface. For example, because SqlProductRepository’s interface is an IProductRepository, it’ll end up as a mapping from IProductRepository to SqlProductRepository.
This particular convention scans the assembly that contains the data access components. You could get a reference to that assembly in many ways, but the easiest way is to pick a representative type, such as SqlProductRepository, and get the assembly from that, as shown in listing 12.10. You could also have chosen a different class or found the assembly by name.
Comparing this convention against the four registrations in listing 12.9, you may think that the benefits of this convention look negligible. Indeed, because there are only four data access components in the current example, the amount of code statements has increased with the convention. But this convention scales much better. Once you write it, it handles hundreds of components without any additional effort.
You can also address the other mappings from listings 12.6 and 12.8 with conventions, but there wouldn’t be much value in doing so. As an example, you can register all services with this convention:
var assembly = typeof(ProductService).Assembly;
var serviceTypes =
from type in assembly.GetTypes()
where !type.Abstract
where type.Name.EndsWith("Service")
select type;
foreach (Type type in serviceTypes)
{
container.Register(type.GetInterfaces().Single(), type);
}
This convention scans the identified assembly for all concrete classes where the name ends with Service and registers each type against the interface it implements. This effectively registers ProductService against the IProductService interface, but because you currently don’t have any other matches for this convention, nothing much is gained. It’s only when more services are added, as indicated in listing 12.9, that it starts to make sense to formulate a convention.
Defining conventions by hand with the use of LINQ might make sense for types all deriving from their own interface, as you’ve seen previously with the repositories. But when you start to register types that are based on a generic interface, as we extensively discussed in section 10.3.3, this strategy starts to break down rather quickly — querying generic types through reflection is typically not a pleasant thing to do.6
That’s why Simple Injector’s Auto-Registration API is built around the registration of types based on a generic Abstraction, such as the ICommandService<TCommand> interface from listing 10.12. Simple Injector allows the registration of all ICommandService<TCommand> implementations to be done in a single line of code.
By supplying a list of assemblies to one of its Register overloads, Simple Injector iterates through these assemblies to find any non-generic, concrete types that implement ICommandService<TCommand>, while registering each type by its specific ICommandService<TCommand> interface. This has the generic type argument TCommand filled in with an actual type.
在具有四个ICommandService<TCommand>实现的应用程序中,先前的 API 调用将等效于以下配置即代码清单。
In an application with four ICommandService<TCommand> implementations, the previous API call would be equivalent to the following Configuration as Code listing.
Iterating a list of assemblies to find appropriate types, however, isn’t the only thing you can achieve with Simple Injector’s Auto-Registration API. Another powerful feature is the registration of generic Decorators, like the ones you saw in listings 10.15, 10.16, and 10.19. Instead of manually composing the hierarchy of Decorators, as you did in listing 10.21, Simple Injector allows Decorators to be applied using its RegisterDecorator method overloads.
Listing 12.13 Registering generic Decorators using Auto-Registration
container.RegisterDecorator( ① typeof(ICommandService<>), ① typeof(AuditingCommandServiceDecorator<>)); ① ① container.RegisterDecorator( ① typeof(ICommandService<>), ① typeof(TransactionCommandServiceDecorator<>)); ① ① container.RegisterDecorator( ① typeof(ICommandService<>), ① typeof(SecureCommandServiceDecorator<>)); ①
Simple Injector applies Decorators in order of registration, which means that, in respect to listing 12.13, the auditing Decorator is wrapped using the transaction Decorator, and the transaction Decorator is wrapped with the security Decorator, resulting in an object graph identical to the one shown in listing 10.21.
Registration of open-generic types can be seen as a form of Auto-Registration because a single method call to RegisterDecorator can result in a Decorator being applied to many registrations.7 Without this form of Auto-Registration for generic Decorator classes, you’d be forced to register each closed version of each Decorator for each closed ICommandService<TCommand> implementation individually, as the following listing shows.
In a system that adheres to the SOLID principles, you create many small and focused classes, but existing classes are less likely to change, increasing maintainability. Auto-Registration prevents the Composition Root from constantly being updated. It’s a powerful technique that has the potential to make the DI Container invisible. Once appropriate conventions are in place, you may have to modify the container configuration only on rare occasions.
12.2.4 混合和匹配配置方法
12.2.4 Mixing and matching configuration approaches
到目前为止,您已经看到了三种不同的配置DI 容器的方法:
So far, you’ve seen three different approaches to configuring a DI Container:
配置文件
Configuration files
配置即代码
Configuration as Code
自动注册
Auto-Registration
这些都不是相互排斥的。您可以选择将Auto-Registration与特定的抽象到具体类型的映射混合使用,甚至可以混合使用所有三种方法来获得一些Auto-Registration、一些Configuration as Code以及配置文件中的一些配置以用于后期绑定目的。
None of these are mutually exclusive. You can choose to mix Auto-Registration with specific mappings of abstract-to-concrete types, and even mix all three approaches to have some Auto-Registration, some Configuration as Code, and some of the configuration in configuration files for late binding purposes.
As a rule of thumb, you should prefer Auto-Registration as a starting point, complemented by Configuration as Code to handle more special cases. You should reserve configuration files for cases where you need to be able to vary an implementation without recompiling the application — which is rarer than you may think.
Now that we’ve covered how to configure a DI Container and how to resolve object graphs with one, you should have a good idea about how to use them. Using a DI Container is one thing, but understanding when to use one is another.
In the previous parts of this book, we solely used Pure DI as our method of Object Composition. This wasn’t just for educational purposes. Complete applications can be built using Pure DI alone.
In section 12.2, we talked about the different configuration methods of DI Containers and how the use of Auto-Registration can increase maintainability of your Composition Root. But the use of DI Containers comes with additional costs and disadvantages over Pure DI. Most, if not all, DI Containers are open source, so they’re free in a monetary sense. But because developer hours are typically the most expensive part of software development, anything that increases the time it takes to develop and maintain software is a cost, which is what we’ll talk about here.
In this section, we’ll compare the advantages and disadvantages, so you can make an educated decision about when to use a DI Container and when to stick to Pure DI. Let’s start with an often overlooked aspect of using libraries such as DI Containers, which is that they introduce costs and risks.
12.3.1 使用第三方库涉及成本和风险
12.3.1 Using third-party libraries involves costs and risks
When a library is free in a monetary sense, we developers often tend to ignore the other costs involved in using it. A DI Container might be considered a Stable Dependency (section 1.3.1), so from a DI perspective, using one isn’t an issue. But there are other concerns to consider. As with any third-party library, using a DI Container comes with costs and risks.
The most obvious cost of any library is its learning curve — it takes time to learn to use a new library. You have to learn its API, its behavior, its quirks, and its limitations. When you’re with a team of developers, most of them will have to understand how to work with that library in one way or another. Having just one developer that knows how to work with the tool might save costs in the short run, but such a practice is in itself a liability to the continuity of your project.8
A library’s behavior, quirks, and limitations might not exactly suit your needs. A library might be opinionated towards a different model than the one your software is built around.9 This is typically something you only find out while you’re learning to use it. As you apply it to your code base, you may find that you need to implement various workarounds. This can result in much yak shaving.
因此,很难估计使用新库将为项目节省多少资金,因为学习成本通常难以实际估计。花在学习第三方库 API 上的累积时间不是构建应用程序本身所花费的时间,因此代表了实际成本。
It is, therefore, hard to estimate how much money the use of a new library will save the project because of the learning costs that are often hard to realistically estimate. The accumulated time spent on learning the API of a third-party library is time not spent building the application itself, and therefore represents a real cost.
Besides the direct cost of learning to work with a library, there are risks involved in taking a dependency on such a library. One risk is that the developers stop maintaining and supporting a library you’re using.10 When such an event occurs, it introduces extra costs to the project because it can force you to switch libraries. In that case, you’re paying the previously discussed learning costs all over again with the additional costs of migrating and testing the application again.
This all sounds like an argument against using external libraries, but that isn’t the case. You wouldn’t be productive without external libraries, because you’d have to reinvent the wheel. If not using an external library means building such a library yourself, you’ll often be worse off. (And we developers tend to underestimate the time it takes to write, test, and maintain such a piece of software.)
With DI Containers, however, you’re in a somewhat different situation. That’s because the alternative to using an external DI Container library isn’t to build your own, but to apply Pure DI.
正如您在 4.1 节中了解到的,与DI 容器的交互应仅限于Composition Root。这已经降低了必须更换时的风险。但即使在那种情况下,替换DI 容器并熟悉新的 API 和设计理念也可能是一项耗时的工作。
As you learned in section 4.1, interaction with the DI Container should be limited to the Composition Root. This already reduces the risk when it must be replaced. But even in that case, it can be a time-consuming endeavor to replace the DI Container and become familiar with a new API and design philosophy.
Pure DI的主要优点是它易于学习。您不必学习任何DI 容器的 API ,尽管个别类仍在使用 DI,但一旦您找到Composition Root,就会清楚发生了什么以及对象图是如何构建的。尽管较新的 IDE 减少了这个问题,但团队中的新开发人员可能很难了解构造的对象图并在使用DI 容器时找到类的依赖项的实现。
The major advantage of Pure DI is that it’s easy to learn. You don’t have to learn the API of any DI Container and, although individual classes still use DI, once you find the Composition Root, it’ll be evident what’s going on and how object graphs are constructed. Although newer IDEs make this less of a problem, it can be difficult for a new developer on a team to get a sense of the constructed object graph and to find the implementation for a class’s Dependency when a DI Container is used.
With Pure DI, this is less of a problem, because object graph construction is hard coded in the Composition Root. Besides being easier to learn, Pure DI gives you a shorter feedback cycle in case there’s an error in your composition of objects. Let’s look at that next.
12.3.2 纯 DI反馈周期更短
12.3.2 Pure DI gives a shorter feedback cycle
DI 容器技术,例如Auto-Wiring和Auto-Registration,依赖于反射的使用。这意味着,在运行时,DI 容器将使用反射分析构造函数参数,甚至通过完整的程序集进行查询,以根据约定查找类型,从而组成完整的对象图。因此,只有在解析对象图时才会在运行时检测到配置错误。与Pure DI相比,DI Container承担了编译器对代码校验的作用。
DI Container techniques, such as Auto-Wiring and Auto-Registration, depend on the use of reflection. This means that, at runtime, the DI Container will analyze constructor arguments using reflection or even query through complete assemblies to find types based on conventions in order to compose complete object graphs. Consequently, configuration errors are only detected at runtime when an object graph is resolved. Compared to Pure DI, the DI Container assumes the compiler’s role of code verification.
When a Composition Root is well structured so that the creation of Singletons and Scoped instances are separated (see listings 8.10 and 8.13, for instance), it allows the compiler to detect Captive Dependencies, as discussed in section 8.4.1.
As we discussed in section 3.2.2, because of strong typing, Pure DI also has the advantage of giving you a clearer picture of the structure of the application’s object graphs. This is something that you’ll lose immediately when you start using a DI Container.
But strong typing cuts both ways because, as we discussed in section 12.1.3, it also means that every time you refactor a constructor, you’ll break the Composition Root. If you’re sharing a library (domain model, utility, data access component, and so on) between applications, you may have more than one Composition Root to maintain. How much of a burden this is depends on how often you refactor constructors, but we’ve seen projects where this happens several times each day. With multiple developers working on a single project, this can easily lead to merge conflicts, which cost time to fix.
Although the compiler will give rapid feedback when using Pure DI, the amount of validations it can do is limited. It’ll be able to report missing Dependencies due to changes to constructors and to some extent Captive Dependencies, but, among other things, it will fail to detect the following:
由于从构造函数体内抛出异常而导致构造函数调用失败(例如,失败的 Guard 子句)
Failing constructor invocations due to exceptions thrown from within the constructor’s body (for example, failing Guard Clauses)
一次性组件超出范围时是否进行处理
Whether disposable components are disposed of when they go out of scope
When classes that are supposed to be Singleton or Scoped are again (accidentally) created in a different part of the Composition Root, possibly with a different lifestyle12
When using Pure DI, the size of the Composition Root grows linearly with the size of the application. When an application is small, its Composition Root will also be small. This makes its Composition Root clean and manageable, and previously listed defects will be easy to spot. But when the Composition Root grows, it becomes easier to miss such defects.
This is something that the use of a DI Container can mitigate. Most DI Containers automatically detect a disposable component on your behalf and might detect common pitfalls, such as Captive Dependencies.13
12.3.3 结论:何时使用DI 容器
12.3.3 The verdict: When to use a DI Container
如果您使用DI Container的Configuration as Code功能(如 12.2.2 节所述),使用容器的 API 显式注册每个组件,您将失去来自强类型的快速反馈。另一方面,维护负担也可能因为Auto-Wiring而下降。尽管如此,您仍然需要在引入每个新类时注册它,这是一个线性增长,并且您和您的团队必须学习该容器的特定 API。但即使您已经熟悉它的 API,仍然存在有一天不得不更换它的风险。你可能失去的比得到的更多。
If you use a DI Container’s Configuration as Code abilities (as discussed in section 12.2.2), explicitly registering each and every component using the container’s API, you lose the rapid feedback from strong typing. On the other hand, the maintenance burden is also likely to drop because of Auto-Wiring. Still, you’ll need to register each new class when you introduce it, which is a linear growth, and you and your team have to learn the specific API of that container. But even if you’re already familiar with its API, there’s still the risk of having to replace it someday. You might lose more than you gain.
Ultimately, if you can wield a DI Container in a sufficiently sophisticated way, you can use it to define a set of conventions using Auto-Registration (as discussed in section 12.2.3). These conventions define a rule set that your code should adhere to, and as long as you stick to those rules, things just work. The container drops to the background, and you rarely need to touch it.
Auto-Registration takes time to learn, and is weakly typed, but, if done right, it enables you to focus on code that adds value instead of infrastructure. An additional advantage is that it creates a positive feedback mechanism, forcing a team to produce code that’s consistent with the conventions. Figure 12.6 visualizes the trade-off between Pure DI and using a DI Container.
As we stated in section 12.2.4, none of the available approaches are mutually exclusive. Although you might find a single Composition Root to contain a mix of all configuration styles, a Composition Root should either be focused around Pure DI with, perhaps, a few late-bound types, or around Auto-Registration with, optionally, a limited amount of Configuration as Code, Pure DI, and configuration files. A Composition Root that focuses around Configuration as Code is pointless and should therefore be avoided.
图 12.6 纯 DI 之所以有价值,是因为它很简单,尽管DI 容器可能是有价值的,也可能是毫无意义的,这取决于它的使用方式。当它以足够复杂的方式使用时(使用自动注册),我们认为DI 容器可以提供最佳的价值/成本比。
Figure 12.6 Pure DI can be valuable because it’s simple, although a DI Container can be either valuable or pointless, depending on how it’s
used. When it’s used in a sufficiently sophisticated way (using Auto-Registration), we consider a DI Container to offer the best value/cost ratio.
The question then becomes this: when should you choose Pure DI, and when should you use Auto-Registration? We, unfortunately, can’t give any hard numbers on this. It depends on the size of the project, the amount of experience you and your team have with a DI Container, and the calculation of risk.
不过,一般来说,您应该对较小的组合根使用纯 DI ,并在维护此类组合根成为问题时切换到自动注册。具有许多类的更大的应用程序可以通过多个约定捕获,可以从使用自动注册中受益。14
In general, though, you should use Pure DI for Composition Roots that are small and switch to Auto-Registration when maintaining such a Composition Root becomes a problem. Bigger applications with many classes that can be captured by several conventions can benefit from using Auto-Registration.14
我们不会告诉您的另一件事是选择哪个DI 容器。选择DI 容器涉及的不仅仅是技术评估。您还必须评估许可模型是否可以接受,您是否信任开发和维护DI 容器的人员或组织,它如何适合您组织的 IT 战略,等等。您对合适的DI 容器的搜索也不应该仅限于本书中列出的容器。例如,许多优秀的 .NET 平台的DI 容器可供选择。
The other thing we won’t tell you is which DI Container to choose. Selecting a DI Container involves more than technical evaluation. You must also evaluate whether the licensing model is acceptable, whether you trust the people or organization that develops and maintains the DI Container, how it fits into your organization’s IT strategy, and so on. Your search for the right DI Container also shouldn’t be limited to the containers listed in this book. For example, many excellent DI Containers for the .NET platform are available to choose from.
如果正确使用DI 容器,它可能是一个有用的工具。最重要的是要了解 DI 的使用决不依赖于DI Container的使用。一个应用程序可以由许多松散耦合的类和模块组成,而这些模块中没有一个对容器一无所知。确保应用程序代码不知道任何DI 容器的最有效方法是将其使用限制在Composition Root中。这可以防止您无意中应用服务定位器反模式,因为它将容器限制在代码的一个小的、孤立的区域。
A DI Container can be a helpful tool if you use it correctly. The most important thing to understand is that the use of DI in no way depends on the use of a DI Container. An application can be made from many loosely coupled classes and modules, and none of these modules knows anything about a container. The most effective way to make sure that application code is unaware of any DI Container is by limiting its use to the Composition Root. This prevents you from inadvertently applying the Service Locator anti-pattern, because it constrains the container to a small, isolated area of the code.
Used in this way, a DI Container becomes an engine that takes care of part of the application’s infrastructure. It composes object graphs based on its configuration. This can be particularly beneficial if you employ Convention over Configuration. If suitably implemented, it can take care of composing object graphs, and you can concentrate your efforts on implementing new features. The container will automatically discover new classes that follow the established conventions and make them available to consumers. The final three chapters of this book cover Autofac (chapter 13), Simple Injector (chapter 14), and Microsoft.Extensions.DependencyInjection (chapter 15).
概括
Summary
DI 容器是提供 DI 功能的库。它是一个解析和管理对象图的引擎。
A DI Container is a library that provides DI functionality. It’s an engine that resolves and manages object graphs.
DI 决不依赖于DI Container的使用。DI 容器是一个有用但可选的工具。
DI in no way hinges on the use of a DI Container. A DI Container is a useful, but optional, tool.
Auto-Wiring is the ability to automatically compose an object graph from maps between Abstractions and concrete types by making use of the type information as supplied by the compiler and the Common Language Runtime (CLR).
构造函数注入静态地公布类的依赖关系要求,DI 容器使用该信息自动连接复杂的对象图。
Constructor Injection statically advertises the Dependency requirements of a class, and DI Containers use that information to Auto-Wire complex object graphs.
Auto-Wiring使Composition Root更能适应变化。
Auto-Wiring makes a Composition Root more resilient to change.
When you start using a DI Container, you’re not required to abandon hand wiring object graphs altogether. You can use hand wiring in parts of your configuration when this is more convenient.
使用DI Container时,三种配置样式是配置文件、Configuration as Code和Auto-Registration。
When using a DI Container, the three configuration styles are configuration files, Configuration as Code, and Auto-Registration.
Configuration files are as much a part of your Composition Root as Configuration as Code and Auto-Registration. Using configuration files, therefore, doesn’t make your Composition Root smaller, it just moves it.
As your application grows in size and complexity, so will your configuration file. Configuration files tend to become brittle and opaque to errors, so only use this approach when you need late binding.
Don’t let the absence of support for handling configuration files influence your choice for picking a DI Container. Types can be loaded from configuration files in a few simple statements.
Configuration as Code allows the container’s configuration to be stored as source code. Each mapping between an Abstraction and a particular implementation is expressed explicitly and directly in code. This method is preferred over configuration files unless you need late binding.
Convention over Configuration 是将约定应用于您的代码,以方便注册。
Convention over Configuration is the application of conventions to your code to facilitate easier registration.
Auto-Registration is the ability to automatically register components in a container by scanning one or more assemblies for implementations of desired Abstractions, which is a form of Convention over Configuration.
Auto-Registration有助于避免不断更新Composition Root,因此比Configuration as Code更受欢迎。
Auto-Registration helps avoid constantly updating the Composition Root and is, therefore, preferred over Configuration as Code.
使用DI 容器等外部库会产生成本和风险;例如,学习新 API 的成本和库被遗弃的风险。
Using external libraries such as DI Containers incurs costs and risks; for example, the cost of learning a new API and the risk of the library being abandoned.
Avoid building your own DI Container. Either use one of the existing, well-tested, and freely available DI Containers, or practice Pure DI. Creating and maintaining such a library takes a lot of effort, which is effort not spent producing business value.
Pure DI的一大优点是它是强类型的。这允许编译器提供有关正确性的反馈,这是您可以获得的最快反馈。
The big advantage of Pure DI is that it’s strongly typed. This allows the compiler to provide feedback about correctness, which is the fastest feedback that you can get.
您应该对较小的组合根使用纯 DI ,并在维护此类组合根成为问题时切换到自动注册。具有许多类的更大的应用程序可以通过多个约定捕获,可以从使用自动注册中获益匪浅。
You should use Pure DI for Composition Roots that are small and switch to Auto-Registration whenever maintaining such Composition Roots becomes a problem. Bigger applications with many classes that can be captured by several conventions can greatly benefit from using Auto-Registration.
13
Autofac DI 容器
13
The Autofac DI Container
在这一章当中
In this chapter
使用 Autofac 的基本注册 API
Working with Autofac’s basic registration API
管理组件生命周期
Managing component lifetime
配置困难的 API
Configuring difficult APIs
配置序列、装饰器和组合
Configuring sequences, Decorators, and Composites
在前面的章节中,我们讨论了一般适用于 DI 的模式和原则,但是,除了几个示例之外,我们还没有详细了解如何使用任何特定的DI 容器来应用它们。在本章中,您将看到这些总体模式如何映射到 Autofac。您需要熟悉前几章的材料才能从中充分受益。
In the previous chapters, we discussed patterns and principles that apply to DI in general, but, apart from a few examples, we’ve yet to take a detailed look at how to apply them using any particular DI Container. In this chapter, you’ll see how these overall patterns map to Autofac. You’ll need to be familiar with the material from the previous chapters to fully benefit from this.
Autofac is a fairly comprehensive DI Container that offers a carefully designed and consistent API. It’s been around since late 2007 and is, at the time of writing, one of the most popular containers.1
In this chapter, we’ll examine how Autofac can be used to apply the principles and patterns presented in parts 1–3. This chapter is divided into four sections. You can read each section independently, though the first section is a prerequisite for the other sections, and the fourth section relies on some methods and classes introduced in the third section.
This chapter should enable you to get started, as well as deal with the most common issues that can come up as you use Autofac on a daily basis. It’s not a complete treatment of Autofac; that would take several more chapters or perhaps a whole book in itself. If you want to know more about Autofac, the best place to start is at the Autofac home page at https://autofac.org.
In this section, you’ll learn where to get Autofac, what you get, and how you start using it. We’ll also look at common configuration options. Table 13.1 provides fundamental information that you’re likely to need to get started.
Using Autofac isn’t that different from using the other DI Containers that we’ll discuss in the following chapters. As with Simple Injector and Microsoft.Extensions.DependencyInjection, usage is a two-step process, as figure 13.1 illustrates. First, you configure a ContainerBuilder, and when you’re done with that, you use it to build a container to resolve components.
Figure 13.1 The pattern for using Autofac is to first configure it, and then resolve components.
完成本节后,您应该对 Autofac 的整体使用模式有一个良好的感觉,并且您应该能够在行为良好的场景中开始使用它——所有组件都遵循正确的 DI 模式,如构造函数注入。让我们从最简单的场景开始,看看如何使用 Autofac 容器解析对象。
When you’re done with this section, you should have a good feeling for the overall usage pattern of Autofac, and you should be able to start using it in well-behaved scenarios — where all components follow proper DI patterns like Constructor Injection. Let’s start with the simplest scenario and see how you can resolve objects using an Autofac container.
The core service of any DI Container is to compose object graphs. In this section, we’ll look at the API that lets you compose object graphs with Autofac.
By default, Autofac requires you to register all relevant components before you can resolve them. This behavior, however, is configurable. The following listing shows one of the simplest possible uses of Autofac.
As figure 13.1 shows, you need a ContainerBuilder instance to configure components. Here, you register the concrete SauceBéarnaise class with builder so that when you ask it to build a container, the resulting container is configured with the SauceBéarnaise class. This again enables you to resolve the SauceBéarnaise class from the container.
With Autofac, however, you never resolve from the root container itself, but from a lifetime scope. Section 13.2.1 goes into more detail about lifetime scope and why resolving from the root container is a bad thing.
The requested service "Ploeh.Samples.MenuModel.SauceBéarnaise" has not been registered. To avoid this exception, either register a component to provide the service, check for service registration using IsRegistered(), or use the ResolveOptional() method to resolve an optional dependency.
Not only can Autofac resolve concrete types with parameterless constructors, it can also Auto-Wire a type with other Dependencies. All these Dependencies need to be registered. For the most part, you’ll want to program to interfaces, because this introduces loose coupling. To support this, Autofac lets you map Abstractions to concrete types.
Whereas your application’s root types will typically be resolved by their concrete types, loose coupling requires you to map Abstractions to concrete types. Creating instances based on such maps is the core service offered by any DI Container, but you must still define the map. In this example, you map the IIngredient interface to the concrete SauceBéarnaise class, which allows you to successfully resolve IIngredient:
var builder = new ContainerBuilder();
builder.RegisterType<SauceBéarnaise>()
.As<IIngredient>(); ①
IContainer container = builder.Build();
ILifetimeScope scope = container.BeginLifetimeScope();
IIngredient sauce = scope.Resolve<IIngredient>(); ②
The As<T> method allows a concrete type to be mapped to a particular Abstraction. Because of the previous As<IIngredient>() call, SauceBéarnaise can now be resolved as IIngredient.
As you saw in listing 13.1, you can stop right there if you only want to register the SauceBéarnaise class. You can also continue with the As method to define how the concrete type should be registered.2
在许多情况下,通用 API 就是您所需要的。尽管它不提供与某些其他DI 容器相同程度的类型安全性,但它仍然是配置容器的可读方式。不过,在某些情况下,您需要一种更弱类型的方式来解析服务。使用 Autofac,这也是可能的。
In many cases, the generic API is all you need. Although it doesn’t offer the same degree of type safety as some other DI Containers, it’s still a readable way to configure the container. Still, there are situations where you need a more weakly typed way to resolve services. With Autofac, this is also possible.
解决弱类型服务
Resolving weakly typed services
有时您不能使用通用 API,因为您在设计时不知道合适的类型。您只有一个Type实例,但您仍然希望获得该类型的实例。您在第 7.3 节中看到了一个示例,其中我们讨论了 ASP.NET Core MVC 的IControllerActivator类. 相关方法是这样的:
Sometimes you can’t use a generic API, because you don’t know the appropriate type at design time. All you have is a Type instance, but you’d still like to get an instance of that type. You saw an example of that in section 7.3, where we discussed ASP.NET Core MVC’s IControllerActivator class. The relevant method is this:
As shown previously in listing 7.8, the ControllerContext captures the controller’s Type, which you can extract using the ControllerTypeInfo property of the ActionDescriptor property:
Type controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType();
Because you only have a Type instance, you can’t use the generic Resolve<T> method, but must resort to a weakly typed API. Autofac offers a weakly typed overload of the Resolve method that lets you implement the Create method like this:
Type controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType();
return scope.Resolve(controllerType);
的弱类型重载Resolve允许您传递controllerType变量直接到Autofac。通常,这意味着您必须将返回值转换为某种抽象,因为弱类型Resolve方法返回object。但是,在 的情况下IControllerActivator,这不是必需的,因为 ASP.NET Core MVC 不需要控制器来实现任何接口或基类。
The weakly typed overload of Resolve lets you pass the controllerType variable directly to Autofac. Typically, this means you have to cast the returned value to some Abstraction, because the weakly typed Resolve method returns object. In the case of IControllerActivator, however, this isn’t required, because ASP.NET Core MVC doesn’t require controllers to implement any interface or base class.
No matter which overload of Resolve you use, Autofac guarantees that it’ll return an instance of the requested type or throw an exception if there are Dependencies that can’t be satisfied. When all required Dependencies have been properly configured, Autofac can Auto-Wire the requested type.
In the previous example, scope is an instance of Autofac.ILifetimeScope. To be able to resolve the requested type, all loosely coupled Dependencies must have been previously configured. There are many ways to configure Autofac, and the next section reviews the most common ones.
As we discussed in section 12.2, you can configure a DI Container in several conceptually different ways. Figure 12.5 reviewed the options: configuration files, Configuration as Code, and Auto-Registration. Figure 13.2 shows these options again.
Figure 13.2 The most common ways to configure a DI Container shown against dimensions of explicitness and the degree of binding
核心配置 API 以代码为中心,同时支持Configuration as Code和基于约定的Auto-Registration。可以使用 Autofac.Configuration NuGet 包插入对配置文件的支持。Autofac 支持所有三种方法,并允许您将它们全部混合在同一个容器中。在本节中,您将看到如何使用这三种类型的配置源中的每一种。
The core configuration API is centered on code and supports both Configuration as Code and convention-based Auto-Registration. Support for configuration files can be plugged in using the Autofac.Configuration NuGet package. Autofac supports all three approaches and lets you mix them all within the same container. In this section, you’ll see how to use each of these three types of configuration sources.
配置ContainerBuilder使用配置作为代码
Configuring the ContainerBuilder using Configuration as Code
In section 13.1, you saw a brief glimpse of Autofac’s strongly typed configuration API. Here, we’ll examine it in greater detail.
Autofac 中的所有配置都使用该类公开的 API ContainerBuilder,尽管您使用的大多数方法都是扩展方法。最常用的方法之一是RegisterType方法你已经看到了:
All configurations in Autofac use the API exposed by the ContainerBuilder class, although most of the methods you use are extension methods. One of the most commonly used methods is the RegisterType method that you’ve already seen:
Registering SauceBéarnaise as IIngredient hides the concrete class so that you can no longer resolve SauceBéarnaise with this registration. But you can easily fix this by using an overload of the As method that lets you specify that the concrete type maps to more than one registered type:
Instead of registering the class only as IIngredient, you can register it as both itself and the interface it implements. This enables the container to resolve requests for both SauceBéarnaise and IIngredient. As an alternative, you can also chain calls to the As method:
Three generic overloads of the As method let you specify one, two, or three types. If you need to specify more, there’s also a non-generic overload that you can use to specify as many types as you like.
In real applications, you always have more than one Abstraction to map, so you must configure multiple mappings. This is done with multiple calls to RegisterType:
This example maps IIngredient to SauceBéarnaise, and ICourse to Course. There’s no overlap of types, so it should be pretty evident what’s going on. But you can also register the same Abstraction several times:
Here, you register IIngredient twice. If you resolve IIngredient, you get an instance of Steak. The last registration wins, but previous registrations aren’t forgotten. Autofac handles multiple configurations for the same Abstraction well, but we’ll get back to this topic in section 13.4.
There are more-advanced options available for configuring Autofac, but you can configure an entire application with the methods shown here. But to save yourself from too much explicit maintenance of container configuration, you could instead consider a more convention-based approach using Auto-Registration.
配置ContainerBuilder使用自动注册
Configuring the ContainerBuilder using Auto-Registration
In many cases, registrations will be similar. Such registrations are tedious to maintain, and explicitly registering each and every component might not be the most productive approach, as we discussed in section 12.3.3.
Consider a library that contains many IIngredient implementations. You can configure each class individually, but it’ll result in numerous similar-looking calls to the RegisterType method. What’s worse is that every time you add a new IIngredient implementation, you must also explicitly register it with the ContainerBuilder if you want it to be available. It’d be more productive to state that all implementations of IIngredient found in a given assembly should be registered.
This is possible using the RegisterAssemblyTypes method. This method lets you specify an assembly and configure all selected classes from this assembly into a single statement. To get the Assembly instance, you can use a representative class (in this case, Steak):
RegisterAssemblyTypes方法_返回与方法相同的接口RegisterType,因此可以使用许多相同的配置选项。这是一个强大的功能,因为这意味着您无需学习新的 API 即可使用自动注册。
The RegisterAssemblyTypes method returns the same interface as the RegisterType method, so many of the same configuration options are available. This is a strong feature, because it means that you don’t have to learn a new API to use Auto-Registration.
In the previous example, we used the As method to register all types in the assembly as IIngredient services. The previous example also unconditionally configures all implementations of the IIngredient interface, but you can provide filters that let you select only a subset. Here’s a convention-based scan where you add only classes whose name starts with Sauce:
When you register all types in an assembly, you can use a predicate to define a selection criterion. The only difference from the previous code example is the inclusion of the Where method, where you select only those types whose names start with Sauce.
There are many other methods that let you provide various selection criteria. The Where method gives you a filter that only lets those types through that match the predicate, but there’s also an Except method that works the other way around.
Apart from selecting the correct types from an assembly, another part of Auto-Registration is defining the correct mapping. In the previous examples, we used the As method with a specific interface to register all selected types against that interface. But sometimes you’ll want to use different conventions.
Let’s say that instead of interfaces, you use abstract base classes, and you want to register all types in an assembly where the name ends with Policy. For this purpose, there are several other overloads of the As method, including one that takes a Func<Type, Type> as input:
您可以为名称以PolicyAs结尾的每个类型使用提供给该方法的代码块。这确保所有带有Policy后缀的类将针对它们的基类进行注册,以便在请求基类时,容器会将其解析为按此约定映射的类型。使用 Autofac 进行基于约定的注册非常简单,并且使用的 API 与 singularRegisterType方法公开的 API 非常相似。
You can use the code block provided to the As method for every single type whose name ends with Policy. This ensures that all classes with the Policy suffix will be registered against their base class, so that when the base class is requested, the container will resolve it to the type mapped by this convention. Convention-based registration with Autofac is surprisingly easy and uses an API that closely mirrors the API exposed by the singular RegisterType method.
通用抽象的自动注册使用AsClosedTypesOf
Auto-Registration of generic Abstractions using AsClosedTypesOf
During the course of chapter 10, you refactored the big, obnoxious IProductService interface to the ICommandService<TCommand> interface of listing 10.12. Here’s that Abstraction again:
public interface ICommandService<TCommand>
{
void Execute(TCommand command);
}
As discussed in chapter 10, every command Parameter Object represents a use case and, apart from any Decorators that implement Cross-Cutting Concerns, there’ll be a single implementation per use case. The AdjustInventoryService of listing 10.8 was given as an example. It implemented the “adjust inventory” use case. The next listing shows this class again.
Any reasonably complex system will easily implement hundreds of use cases. This is an ideal candidate for using Auto-Registration. With Autofac, this couldn’t be easier, as the following listing shows.
As in the previous listings, you make use of the RegisterAssemblyTypes method to select classes from the supplied assembly. Instead of calling As, however, you call AsClosedTypesOf and supply the open-generic ICommandService<TCommand> interface.
Using the supplied open-generic interface, Autofac iterates through the list of assembly types and registers all types that implement a closed-generic version of ICommandService<TCommand>. What this means, for instance, is that AdjustInventoryService is registered, because it implements ICommandService<AdjustInventory>, which is a closed-generic version of ICommandService<TCommand>.
The RegisterAssemblyTypes method takes a params array of Assembly instances, so you can supply as many assemblies to a single convention as you’d like. It’s not a far-fetched thought to scan a folder for assemblies and supply them all to implement add-in functionality. In that way, add-ins can be added without recompiling a core application. This is one way to implement late binding; another is to use configuration files.
配置ContainerBuilder使用配置文件
Configuring the ContainerBuilder using configuration files
当您需要在不重新编译应用程序的情况下更改容器的注册时,配置文件是一个可行的选择。正如我们在 12.2.1 节中所述,您应该仅将配置文件用于需要后期绑定的那些类型的 DI 配置:在所有其他类型和配置的所有其他部分中更喜欢配置为代码或自动注册。
When you need to change a container’s registrations without recompiling the application, configuration files are a viable option. As we stated in section 12.2.1, you should use configuration files only for those types of your DI configuration that require late binding: prefer Configuration as Code or Auto-Registration in all other types and all other parts of your configuration.
The most natural way to use configuration files is to embed those into the standard .NET application configuration file. This is possible, but you can also use a standalone configuration file if you need to vary the Autofac configuration independently of the standard .config file. Whether you want to do one or the other, the API is almost the same.
Once you have a reference to Autofac.Configuration, you can ask the ContainerBuilder to read component registrations from the standard .config file like this:
var configuration = new ConfigurationBuilder() ① .AddJsonFile("autofac.json") ① .Build(); ① builder.RegisterModule( ② new ConfigurationModule(configuration)); ②
这是一个将IIngredient接口映射到Steak类的简单示例:
Here’s a simple example that maps the IIngredient interface to the Steak class:
{
"defaultAssembly": "Ploeh.Samples.MenuModel", ①
"components": [
{
"services": [{ ② "type": "Ploeh.Samples.MenuModel.IIngredient" ② }], ② "type": "Ploeh.Samples.MenuModel.Steak" ②
}]
}
The type name must include the namespace so that Autofac can find that type. Because both types are located in the default assembly Ploeh.Samples.MenuModel, the assembly name can be omitted in this case. Although the defaultAssembly attribute is optional, it’s a nice feature that can save you from a lot of typing if you have many types defined in the same assembly.
The components element is a JSON array of component elements. The previous example contained a single component, but you can add as many component elements as you like. In each element, you must specify a concrete type with the type attribute. This is the only required attribute. To map the Steak class to IIngredient, you can use the optional services attribute.
A configuration file is a good option when you need to change the configuration of one or more components without recompiling the application, but because it tends to be quite brittle, you should reserve it for only those occasions. Use either Auto-Registration or Configuration as Code for the main part of the container’s configuration.
本节介绍了 Autofac DI 容器并演示了这些基本机制:如何配置ContainerBuilder以及随后如何使用构建的容器来解析服务。只需调用一次Resolve方法即可轻松完成解析服务,因此复杂性涉及配置容器。这可以通过几种不同的方式完成,包括命令式代码和配置文件。
This section introduced the Autofac DI Container and demonstrated these fundamental mechanics: how to configure a ContainerBuilder and, subsequently, how to use the constructed container to resolve services. Resolving services is easily done with a single call to the Resolve method, so the complexity involves configuring the container. This can be done in several different ways, including imperative code and configuration files.
Until now, we’ve only looked at the most basic API, so there are more-advanced areas we have yet to cover. One of the most important topics is how to manage component lifetime.
13.2 管理生命周期
13.2 Managing lifetime
在第 8 章中,我们讨论了生命周期管理,包括最常见的概念生命周期,例如Singleton、Scoped和Transient。Autofac 支持多种不同的Lifestyles,使您能够配置所有服务的生命周期。表 13.2中显示的生活方式作为 API 的一部分提供。
In chapter 8, we discussed Lifetime Management, including the most common conceptual Lifestyles such as Singleton, Scoped, and Transient. Autofac supports several different Lifestyles, enabling you to configure the lifetime of all services. The Lifestyles shown in table 13.2 are available as part of the API.
Autofac’s implementations of Transient and Singleton are equivalent to the general Lifestyles described in chapter 8, so we won’t spend much time on them in this chapter. Instead, in this section, you’ll see how you can define Lifestyles for components both in code and with configuration files. We’ll also look at Autofac’s concept of lifetime scopes and how they can be used to implement the Scoped Lifestyle. By the end of this section, you should be able to use Autofac’s Lifestyles in your own application. Let’s start by reviewing how to configure instance scopes for components.
In this section, we’ll review how to manage component instance scopes with Autofac. Instance scopes are configured as part of registering components, and you can define them both with code and via a configuration file. We’ll look at each in turn.
使用代码配置实例范围
Configuring instance scopes with code
实例范围定义为您在ContainerBuilder实例上进行的注册的一部分。就这么简单:
Instance scope is defined as part of the registrations you make on a ContainerBuilder instance. It’s as easy as this:
This configures the concrete SauceBéarnaise class as a Singleton so that the same instance is returned each time SauceBéarnaise is requested. If you want to map an Abstraction to a concrete class with a specific lifetime, you can use the usual As method and place the SingleInstance method call wherever you like. These two registrations are functionally equivalent:
Notice that the only difference is that we’ve swapped the As and SingleInstance method calls. Personally, we prefer the sequence on the left, because the RegisterType and As method calls form a mapping between a concrete class and an Abstraction. Keeping them close together makes the registration more readable, and you can then state the instance scope as a modification to the mapping.
尽管Transient是默认的实例范围,但您可以显式声明它。这两个例子是等价的:
Although Transient is the default instance scope, you can explicitly state it. These two examples are equivalent:
You can use SingleInstance and the other related methods to define the instance scope for all registrations in a convention. In the previous example, you defined all IIngredient registrations as Singleton. In the same way that you can register components both in code and in a configuration file, you can also configure instance scope in both places.
使用配置文件配置实例范围
Configuring instance scopes with configuration files
When you need to define components in a configuration file, you might want to configure their instance scopes in the same place; otherwise, it would result in all components getting the same default Lifestyle. This is easily done as part of the configuration schema you saw in section 13.1.2. You can use the optional instance-scope attribute to declare the Lifestyle.
Compared to the example in section 13.1.2, the only difference is the added instance-scope attribute that configures the instance as a Singleton. When you omit the instance-scope attribute, per-dependency is used, which is Autofac’s equivalent to Transient.
Both in code and in a file, it’s easy to configure instance scopes for components. In all cases, it’s done in a rather declarative fashion. Although configuration is easy, you must not forget that some Lifestyles involve long-lived objects that use resources as long as they’re around.
As discussed in section 8.2.2, it’s important to release objects when you’re done with them. Autofac has no explicit Release method but instead uses a concept called lifetime scopes. A lifetime scope can be regarded as a throw-away copy of the container. As figure 13.3 illustrates, it defines a boundary where components can be reused.
A lifetime scope defines a derived container that you can use for a particular duration or purpose; the most obvious example is a web request. You spawn a scope from a container so that the scope inherits all the Singletons tracked by the parent container, but the scope also acts as a container of local Singletons. When a lifetime-scoped component is requested from a lifetime scope, you always receive the same instance. The difference from true Singletons is that if you query a second scope, you’ll get another instance.
One of the important features of lifetime scopes is that they allow you to properly release components when the scope completes. You create a new scope with the BeginLifetimeScope method and release all appropriate components by invoking its Dispose method like so:
using (var scope = container.BeginLifetimeScope()) ①
{
IMeal meal = scope.Resolve<IMeal>(); ② meal.Consume(); ③ } ④
You create a new scope from the container by invoking the BeginLifetimeScope method. The return value implements IDisposable so you can wrap it in a using block. Because it also implements the same interface that the container itself implements, you can use the scope to resolve components in exactly the same way as with the container itself.
When you’re done with a lifetime scope, you can dispose of it. This happens automatically with a using block when you exit the block, but you can also choose to explicitly dispose of it by invoking the Dispose method. When you dispose of a scope, you also release all the components that were created by the lifetime scope. In the example, it means that you release the meal object graph.
Dependencies of a component are always resolved at or below the component’s lifetime scope. For example, if you need a Transient Dependency injected into a Singleton, that Transient Dependency comes from the root container even if you’re resolving the Singleton from a nested lifetime scope. This will track the Transient within the root container and prevent it from being disposed of when the lifetime scope gets disposed of. The Singleton consumer would otherwise break, because it’s kept alive in the root container while depending on a component that was disposed of.
Earlier in this section, you saw how to configure components as Singletons or Transients. Configuring a component to have its instance scope tied to a lifetime scope is done in a similar way:
builder.RegisterType<SauceBéarnaise>()
.As<IIngredient>()
.InstancePerLifetimeScope(); ①
Due to their nature, Singletons are never released for the lifetime of the container itself. Still, you can release even those components if you don’t need the container any longer. This is done by disposing of the container itself:
In practice, this isn’t nearly as important as disposing of a scope because the lifetime of a container tends to correlate closely with the lifetime of the application it supports. You normally keep the container around as long as the application runs, so you’d only dispose of it when the application shuts down. In this case, memory would be reclaimed by the operating system.
This completes our tour of Lifetime Management with Autofac. Components can be configured with mixed instance scopes, and this is true even when you register multiple implementations of the same Abstraction. But until now, you’ve allowed the container to wire Dependencies by implicitly assuming that all components use Auto-Wiring. This isn’t always the case. In the next section, we’ll review how to deal with classes that must be instantiated in special ways.
13.3 注册困难的 API
13.3 Registering difficult APIs
到目前为止,我们已经考虑了如何配置使用构造函数注入的组件。构造函数注入的众多好处之一是像 Autofac 这样的DI 容器可以很容易地理解如何在依赖图中组合和创建所有类。当 API 表现不佳时,这一点就不太清楚了。
Until now, we’ve considered how you can configure components that use Constructor Injection. One of the many benefits of Constructor Injection is that DI Containers like Autofac can easily understand how to compose and create all classes in a Dependency graph. This becomes less clear when APIs are less well behaved.
In this section, you’ll see how to deal with primitive constructor arguments and static factories. These all require special attention. Let’s start by looking at classes that take primitive types, such as strings or integers, as constructor arguments.
As long as you inject Abstractions into consumers, all is well. But it becomes more difficult when a constructor depends on a primitive type, such as a string, a number, or an enum. This is particularly the case for data access implementations that take a connection string as constructor parameter, but it’s a more general issue that applies to all strings and numbers.
Conceptually, it doesn’t always make sense to register a string or number as a component in a container. But with Autofac, this is at least possible. Consider as an example this constructor:
If you want all consumers of Spiciness to use the same value, you can register Spiciness and ChiliConCarne independently of each other. This snippet shows how:
When you subsequently resolve ChiliConCarne, it’ll have a Spiciness value of Medium, as will all other components with a Dependency on Spiciness. If you’d rather control the relationship between ChiliConCarne and Spiciness on a finer level, you can use the WithParameter method. Because you want to supply a concrete value for the Spiciness parameter, you can use the WithParameter overload that takes a parameter name and a value:
Both options described here use Auto-Wiring to provide a concrete value to a component. As discussed in section 13.4, this has advantages and disadvantages. A more convenient solution, however, is to extract the primitive Dependencies into Parameter Objects.
In section 10.3.3, we discussed how the introduction of Parameter Objects allowed mitigating the Open/Closed Principle violation that IProductService caused. Parameter Objects, however, are also a great tool to mitigate ambiguity.
The Spiciness of a course, for instance, could be described in the more general term flavoring. Flavoring might include other properties, such as saltiness. In other words, you can wrap the Spiciness and ExtraSalty in a Flavoring class:4
public class Flavoring
{
public readonly Spiciness Spiciness;
public readonly bool ExtraSalty;
public Flavoring(Spiciness spiciness, bool extraSalty)
{
this.Spiciness = spiciness;
this.ExtraSalty = extraSalty;
}
}
With the introduction of the Flavoring Parameter Object, it now becomes easy to Auto-Wire any ICourse implementation that requires some flavoring:
var flavoring = new Flavoring(Spiciness.Medium, extraSalty: true);
builder.RegisterInstance<Flavoring>(flavoring);
builder.RegisterType<ChiliConCarne>().As<ICourse>();
Now you have a single instance of the Flavoring class. Flavoring becomes a configuration object for ICourses. Because there’ll only be one Flavoring instance, you can register it in Autofac using RegisterInstance.
Extracting primitive Dependencies into Parameter Objects should be your preference over the previously discussed options because Parameter Objects remove ambiguity, both at the functional and the technical levels. It does, however, require a change to a component’s constructor, which might not always be feasible. In this case, the use of WithParameter is your second-best pick.
13.3.2 用代码块注册对象
13.3.2 Registering objects with code blocks
创建具有原始值的组件的另一种选择是使用Register方法。它允许您提供创建组件的委托:
Another option for creating a component with a primitive value is to use the Register method. It lets you supply a delegate that creates the component:
builder.Register<ICourse>(c => new ChiliConCarne(Spiciness.Hot));
You already saw the Register method when we discussed the registration of Spiciness in section 13.3.1. Here, the ChiliConCarne constructor is invoked with a Spiciness value of Hot every time the ICourse service is resolved.
When it comes to the ChiliConCarne class, you have a choice between Auto-Wiring or using a code block. Other classes can be more restrictive: they can’t be instantiated through a public constructor. Instead, you must use some sort of factory to create instances of the type. This is always troublesome for DI Containers, because, by default, they look after public constructors. Consider this example constructor for the public JunkFood class:
Even though the JunkFood class might be public, the constructor is internal. In this example, instances of JunkFood should instead be created through the static JunkFoodFactory class:
public static class JunkFoodFactory
{
public static JunkFood Create(string name)
{
return new JunkFood(name);
}
}
From Autofac’s perspective, this is a problematic API, because there are no unambiguous and well-established conventions around static factories. It needs help — and you can give that help by providing a code block it can execute to create the instance:
This time, you use the Register method to create the component by invoking a static factory within the code block. With that in place, JunkFoodFactory.Create is invoked every time IMeal is resolved and the result returned.
When you end up writing the code to create the instance, how is this better than invoking the code directly? By using a code block inside a Register method call, you still gain something:
你映射从IMeal到JunkFood。这允许消费类保持松散耦合。
You map from IMeal to JunkFood. This allows consuming classes to stay loosely coupled.
Instance scope can still be configured. Although the code block will be invoked to create the instance, it may not be invoked every time the instance is requested. It is by default, but if you change it to a Singleton, the code block will only be invoked once, with the result cached and reused thereafter.
In this section, you’ve seen how you can use Autofac to deal with more-difficult creational APIs. You can use the WithParameter method to wire constructors with services to maintain a semblance of Auto-Wiring, or you can use the Register method with a code block for a more type-safe approach. We have yet to look at how to work with multiple components, so let’s now turn our attention in that direction.
As alluded to in section 12.1.2, DI Containers thrive on distinctness but have a hard time with ambiguity. When using Constructor Injection, a single constructor is preferred over overloaded constructors, because it’s evident which constructor to use when there’s no choice. This is also the case when mapping from Abstractions to concrete types. If you attempt to map multiple concrete types to the same Abstraction, you introduce ambiguity.
Despite the undesirable qualities of ambiguity, you often need to work with multiple implementations of a single Abstraction.5 This can be the case in situations like these:
不同的混凝土类型用于不同的消费者。
Different concrete types are used for different consumers.
In this section, we’ll look at each of these cases and see how Autofac addresses each in turn. When we’re done, you should be able to register and resolve components even when multiple implementations of the same Abstraction are in play. Let’s first see how you can provide more fine-grained control than Auto-Wiring provides.
Auto-Wiring is convenient and powerful but provides little control. As long as all Abstractions are distinctly mapped to concrete types, you have no problems. But as soon as you introduce more implementations of the same interface, ambiguity rears its ugly head. Let’s first recap how Autofac deals with multiple registrations of the same Abstraction.
配置同一服务的多个实现
Configuring multiple implementations of the same service
正如您在 13.1.2 节中看到的,您可以像这样注册同一接口的多个实现:
As you saw in section 13.1.2, you can register multiple implementations of the same interface like this:
This example registers both the Steak and SauceBéarnaise classes as the IIngredient service. The last registration wins, so if you resolve IIngredient with scope.Resolve<IIngredient>(), you’ll get a SauceBéarnaise instance.
You can also ask the container to resolve all IIngredient components. Autofac has no dedicated method to do that, but instead relies on relationship types (https://mng.bz/P429). A relationship type is a type that indicates a relationship that the container can interpret. As an example, you can use IEnumerable<T> to indicate that you want all services of a given type:
Notice that we use the normal Resolve method, but that we request IEnumerable<IIngredient>. Autofac interprets this as a convention and gives us all the IIngredient components it has.
When you register components, you can give each registration a name that you can later use to select among the different components. This code snippet shows that process:
As always, you start with the RegisterType method, but instead of following up with the As method, you use the Named method to specify a service type as well as a name. This enables you to resolve named services by supplying the same name to the ResolveNamed method:
Given that you should always resolve services in a single Composition Root, you should normally not expect to deal with such ambiguity on this level. If you do find yourself invoking the Resolve method with a specific name or key, consider if you can change your approach to be less ambiguous. You can also use named or keyed instances to select among multiple alternatives when configuring Dependencies for a given service.
As useful as Auto-Wiring is, sometimes you need to override the normal behavior to provide fine-grained control over which Dependencies go where; it can also be that you need to address an ambiguous API. As an example, consider this constructor:
public ThreeCourseMeal(ICourse entrée, ICourse mainCourse, ICourse dessert)
In this case, you have three identically typed Dependencies, each of which represents a different concept. In most cases, you want to map each of the Dependencies to a separate type. The following listing shows how you could choose to register the ICourse mappings.
Here, you register three named components, mapping the Rilettes to an instance named entrée, CordonBleu to an instance named mainCourse, and the MousseAuChocolat to an instance named dessert. Given this configuration, you can now register the ThreeCourseMeal class with the named registrations.
This turns out to be surprisingly complex. In the following listing, we’ll first show you what it looks like, and then we’ll subsequently pick apart the example to understand what’s going on.
builder.RegisterType<ThreeCourseMeal>().As<IMeal>()
.WithParameter( ①
(p, c) => p.Name == "entrée",
(p, c) => c.ResolveNamed<ICourse>("entrée"))
.WithParameter(
(p, c) => p.Name == "mainCourse", ②
(p, c) => c.ResolveNamed<ICourse>("mainCourse"))
.WithParameter(
(p, c) => p.Name == "dessert",
(p, c) => c.ResolveNamed<ICourse>("dessert")); ③
Let’s take a closer look at what’s going on here. The WithParameter method overload wraps around the ResolvedParameter class, which has this constructor:
public ResolvedParameter(
Func<ParameterInfo, IComponentContext, bool> predicate,
Func<ParameterInfo, IComponentContext, object> valueAccessor);
The predicate parameter is a test that determines whether the valueAccessor delegate will be invoked. When predicate returns true, valueAccessor is invoked to provide the value for the parameter. Both delegates take the same input: information about the parameter in the form of a ParameterInfo object and an IComponentContext that can be used to resolve other components. When Autofac uses the ResolvedParameter instances, it provides both of these values when it invokes the delegates.
As listing 13.6 shows, the resulting registration is rather verbose. With the aid of two self-written helper methods, however, you can simplify the registration considerably:
By introducing the Named and InjectWith<T> helper methods, you simplified the registration, reduced its verbosity, and at the same time, made it easier to read what’s going on. It almost starts to read like poetry (or a well-aged bottle of wine):
When called, both methods create a new delegate that wraps the supplied name argument. Sometimes there’s no other way than to use the WithParameter method for each and every constructor parameter, but in other cases, you can take advantage of conventions.
If you examine listing 13.6 closely, you’ll notice a repetitive pattern. Each call to WithParameter addresses only a single constructor parameter, but each valueAccessor does the same thing: it uses the IComponentContext to resolve an ICourse component with the same name as the parameter.
There’s no requirement that says you must name the component after the constructor parameter, but when this is the case, you can take advantage of this convention and rewrite listing 13.6 in a simpler way. The following listing demonstrates how.
It might be a little surprising, but you can address all three constructor parameters of the ThreeCourseMeal class with the same WithParameter call. You do that by stating that this instance will handle any parameter Autofac might throw at it. Because you only use this method to configure the ThreeCourseMeal class, the convention only applies within this limited scope.
As the predicate always returns true, the second code block will be invoked for all three constructor parameters. In all three cases, it’ll ask IComponentContext to resolve a component that has the same name and type as the parameter. This is functionally the same as what you did in listing 13.6.
As in listing 13.6, you can create a simplified version of listing 13.7. But we’ll leave this as an exercise for the reader.
覆盖自动装配通过将参数显式映射到命名组件是一种普遍适用的解决方案。即使您在Composition Root的一部分中配置命名组件而在完全不同的部分中配置消费者,您也可以这样做,因为将命名组件与参数联系在一起的唯一标识是名称。这始终是可能的,但如果您要管理许多名称,则可能会很脆弱。当提示您使用命名组件的最初原因是为了处理歧义时,更好的解决方案是设计您自己的 API 来消除这种歧义。它通常会带来更好的整体设计。
Overriding Auto-Wiring by explicitly mapping parameters to named components is a universally applicable solution. You can do this even if you configure the named components in one part of the Composition Root and the consumer in a completely different part, because the only identification that ties a named component together with a parameter is the name. This is always possible but can be brittle if you have many names to manage. When the original reason prompting you to use named components is to deal with ambiguity, a better solution is to design your own API to get rid of that ambiguity. It often leads to a better overall design.
In the next section, you’ll see how to use the less ambiguous and more flexible approach, where you allow any number of courses in a meal. To this end, you must learn how Autofac deals with lists and sequences.
In section 6.1.1, we discussed how Constructor Injection acts as a warning system for Single Responsibility Principle violations. The lesson then was that instead of viewing Constructor Over-Injection as a weakness of the Constructor Injection pattern, you should rather rejoice that it makes problematic design so obvious.
When it comes to DI Containers and ambiguity, we see a similar relationship. DI Containers generally don’t deal with ambiguity in a graceful manner. Although you can make a good DI Container like Autofac deal with it, it can seem awkward. This is often an indication that you could improve on the design of your code.
Instead of feeling constrained by Autofac, you should embrace its conventions and let it guide you toward a better and more consistent design. In this section, we’ll look at an example that demonstrates how you can refactor away from ambiguity, as well as show how Autofac deals with sequences, arrays, and lists.
通过消除歧义重构更好的课程
Refactoring to a better course by removing ambiguity
在 13.4.1 节中,您看到了 theThreeCourseMeal及其固有的歧义如何迫使您放弃自动装配而改用WithParameter. 这应该会促使您重新考虑 API 设计。例如,一个简单的概括走向它的实现IMeal采用任意数量的ICourse实例,而不是恰好三个实例,就像ThreeCourseMeal类的情况一样:
In section 13.4.1, you saw how the ThreeCourseMeal and its inherent ambiguity forced you to abandon Auto-Wiring and instead use WithParameter. This should prompt you to reconsider the API design. For example, a simple generalization moves toward an implementation of IMeal that takes an arbitrary number of ICourse instances, instead of exactly three, as was the case with the ThreeCourseMeal class:
public Meal(IEnumerable<ICourse> courses)
请注意,ICourse构造函数中不需要三个不同的实例,对一个实例的单一依赖IEnumerable<ICourse>使您可以为班级提供任意数量的课程Meal——从零到……很多!这解决了含糊不清的问题,因为现在只有一个Dependency。此外,它还通过提供一个单一的通用类来改进 API 和实现,该类可以模拟不同类型的膳食,从具有单一课程的简单膳食到精心制作的 12 道菜晚餐。
Notice that instead of requiring three distinct ICourse instances in the constructor, the single dependency on an IEnumerable<ICourse> instance lets you provide any number of courses to the Meal class — from zero to ... a lot! This solves the issue with ambiguity, because there’s now only a single Dependency. In addition, it also improves the API and implementation by providing a single, general-purpose class that can model different types of meals, from a simple meal with a single course to an elaborate 12-course dinner.
In this section, we’ll look at how you can configure Autofac to wire up Meal instances with appropriate ICourseDependencies. When you’re done, you should have a good idea of the options available when you need to configure instances with sequences of Dependencies.
Autofac has a good understanding of sequences, so if you want to use all registered components of a given service, Auto-Wiring just works. As an example, you can configure the IMeal service like this:
Notice that this is a completely standard mapping from a concrete type to an Abstraction. Autofac automatically understands the Meal constructor and determines that the correct course of action is to resolve all ICourse components. When you resolve IMeal, you get a Meal instance with the ICourse components: Rillettes, CordonBleu, and MousseAuChocolat.
Autofac automatically handles sequences, and, unless you specify otherwise, it does what you’d expect it to do: it resolves a sequence of Dependencies to all registered components of that type. Only when you need to explicitly pick some components from a larger set do you need to do more. Let’s see how you can do that.
Autofac’s default strategy of injecting all components is often the correct policy, but as figure 13.4 shows, there may be cases where you want to pick only some registered components from the larger set of all registered components.
When you previously let Autofac Auto-Wire all configured instances, it corresponded to the situation depicted on the right side of the figure. If you want to register a component as shown on the left side, you must explicitly define which components should be used. In order to achieve this, you can use the WithParameter method the way you did in listings 13.6 and 13.7. This time, you’re dealing with the Meal constructor that only takes a single parameter. The following listing demonstrates how you can implement the value-providing part of WithParameter to explicitly pick named components from the IComponentContext.
As you saw in section 13.4.1, the WithParameter method takes two delegates as input parameters. The first is a predicate that’s used to determine if the second delegate should be invoked. In this case, you decide to be a bit lazy and return true. You know that the Meal class has only a single constructor parameter, so this’ll work. But if you later refactor the Meal class to take a second constructor parameter, this may not work correctly anymore. It might be safer to define an explicit check for the parameter type.
The second delegate provides the value for the parameter. You use IComponentContext to resolve three named components into an array. The result is an array of ICourse instances, which is compatible with IEnumerable<ICourse>.
Autofac natively understands sequences; unless you need to explicitly pick only some components from all services of a given type, Autofac automatically does the right thing. Auto-Wiring works not only with single instances, but also for sequences, and the container maps a sequence to all configured instances of the corresponding type. A perhaps less intuitive use of having multiple instances of the same Abstraction is the Decorators design pattern, which we’ll discuss next.
In section 9.1.1, we discussed how the Decorator design pattern is useful when implementing Cross-Cutting Concerns. By definition, Decorators introduce multiple types of the same Abstraction. At the very least, you have two implementations of an Abstraction: the Decorator itself and the decorated type. If you stack the Decorators, you can have even more. This is another example of having multiple registrations of the same service. Unlike the previous sections, these registrations aren’t conceptually equal but rather Dependencies of each other.
There are multiple strategies for applying Decorators in Autofac, such as using the previously discussed WithParameter or using code blocks, as we discussed in section 13.3.2. In this section, however, we’ll focus on the use of the RegisterDecorator and RegisterGenericDecorator methods because they make configuring Decorators a no-brainer.
装饰非泛型抽象RegisterDecorator
Decorating non-generic Abstractions with RegisterDecorator
Autofac has built-in support for Decorators via the RegisterDecorator method. The following example shows how to use this method to apply Breading to a VealCutlet:
builder.RegisterType<VealCutlet>() ① .As<IIngredient>(); ① builder.RegisterDecorator<Breading, IIngredient>(); ②
As you learned in chapter 9, you get Cordon Bleu when you slit open a pocket in the veal cutlet and add ham, cheese, and garlic into the pocket before breading the cutlet. The following example shows how to add a HamCheeseGarlic Decorator in between VealCutlet and the Breading Decorator:
builder.RegisterType<VealCutlet>()
.As<IIngredient>();
builder.RegisterDecorator<HamCheeseGarlic, ① IIngredient>(); ①
builder.RegisterDecorator<Breading, IIngredient>();
By placing this new registration before the Breading registration, the HamCheeseGarlic Decorator is wrapped first. This results in an object graph equal to the following Pure DI version:
new Breading( ① new HamCheeseGarlic( ① new VealCutlet())); ①
During the course of chapter 10, we defined multiple generic Decorators that could be applied to any ICommandService<TCommand> implementation. In the remainder of this chapter, we’ll set our ingredients and courses aside, and take a look at how to register these generic Decorators using Autofac. The following listing demonstrates how to register all ICommandService<TCommand> implementations with the three Decorators presented in section 10.3.
As you saw in listing 13.3, listing 13.9 uses RegisterAssemblyTypes to register arbitrary ICommandService<TCommand> implementations. To register generic Decorators, however, Autofac provides a different method — RegisterGenericDecorator. The result of the configuration of listing 13.9 is figure 13.5, which we discussed previously in section 10.3.4.
You can configure Decorators in different ways, but in this section, we focused on Autofac’s methods that were explicitly designed for this task. Autofac lets you work with multiple instances in several different ways: you can register components as alternatives to each other, as peers resolved as sequences, or as hierarchical Decorators. In many cases, Autofac figures out what to do, but you can always explicitly define how services are composed if you need more-explicit control.
Although consumers that rely on sequences of Dependencies can be the most intuitive use of multiple instances of the same Abstraction, Decorators are another good example. But there’s a third and perhaps a bit surprising case where multiple instances come into play, which is the Composite design pattern.
During the course of this book, we discussed the Composite design pattern on several occasions. In section 6.1.2, for instance, you created a CompositeNotificationService (listing 6.4) that both implemented INotificationService and wrapped a sequence of INotificationService implementations.
Let’s take a look at how you can register Composites like the CompositeNotificationService from chapter 6 in Autofac. The following listing shows this class again.
Here, three INotificationService implementations are registered by the same name, service, using the Auto-Wiring API of Autofac. The CompositeNotificationService, on the other hand, is registered using a delegate. Inside the delegate, the Composite is newed up manually and injected with an IEnumerable<INotificationService>. By specifying the service name, the previous named registrations are resolved.
Because the number of notification services will likely grow over time, you can reduce the burden on your Composition Root by applying Auto-Registration. Using the RegisterAssemblyTypes method, you can turn the previous list of registrations in a simple one-liner.
This looks reasonably simple, but looks are deceiving. RegisterAssemblyTypes will register any non-generic implementation that implements INotificationService. When you try to run the previous code, depending on which assembly your Composite is located in, Autofac might throw the following exception:
Autofac detected a cyclic Dependency. (We discussed Dependency cycles in detail in section 6.3.) Fortunately, its exception message is pretty clear. It describes that CompositeNotificationService depends on INotificationService[]. The CompositeNotificationService wraps a sequence of INotificationService, but that sequence itself again contains CompositeNotificationService. What this means is that CompositeNotificationService is an element of the sequence that’s injected into CompositeNotificationService. This is an object graph that’s impossible to construct.
CompositeNotificationService became a part of the sequence because Autofac’s RegisterAssemblyTypes registers all non-generic INotificationService implementations it finds. In this case, CompositeNotificationService was placed in the same assembly as all other implementations.
There are multiple ways around this. The simplest solution is to move the Composite to a different assembly; for instance, the assembly containing the Composition Root. This prevents RegisterAssemblyTypes from selecting the type, because it’s provided with a particular Assembly instance. Another option is to filter the CompositeNotificationService out of the list. An elegant way of doing this is using the Except method:
Composite classes, however, aren’t the only classes that might require removal. You’ll have to do the same for any Decorator. This isn’t particularly difficult, but because there’ll typically be more Decorator implementations, you might be better off querying the type information to find out whether the type represents a Decorator or not. The following example shows how you can filter out Decorators as well, using a custom IsDecoratorFor helper method:
The IsDecoratorFor method expects a type to have a single constructor. A type is considered to be a Decorator when it both implements the given TAbstraction and its constructor also requires a T.
In section 13.4.3, you saw how using Autofac’s RegisterGenericDecorator method made registering generic Decorators child’s play. In this section, we’ll take a look at how you can register Composites for generic Abstractions.
In section 6.1.3, you specified the CompositeEventHandler<TEvent> class (listing 6.12) as a Composite implementation over a sequence of IEventHandler<TEvent> implementations. Let’s see if you can register the Composite with its wrapped event handler implementations.
Let’s start with Auto-Registration of the event handlers. As you’ve seen previously, this is done using the RegisterAssemblyTypes method:
builder.RegisterAssemblyTypes(assembly)
.As(type =>
from interfaceType in type.GetInterfaces()
where interfaceType.IsClosedTypeOf(typeof(IEventHandler<>))
select new KeyedService("handler", interfaceType));
This example makes use of the As overload that allows supplying a sequence of Autofac.Core.KeyedService instances. A KeyedService class is a small data object that combines both a key and a service type.
Autofac runs any type it finds in the assembly through the As method. You can use a LINQ query to find the type’s implemented interface that’s a closed-generic version of IEventHandler<TEvent>. For most types in the assembly, this query won’t yield any results, because most types don’t implement IEventHandler<TEvent>. For those types, no registration is added to ContainerBuilder.
Even though this is quite complex, generic Composites and Decorators don’t have to be filtered out. RegisterAssemblyTypes only selects non-generic implementations. Generic types, such as CompositeEventHandler<TEvent>, won’t cause any problem, and don’t have to be filtered out or moved to a different assembly. This is fortunate, because it wouldn’t be fun at all to have to write a version of IsDecoratorFor that could handle generic Abstractions.
What remains is the registration for CompositeEventHandler<TEvent>. Because this type is generic, you can’t use the Register overload that takes in a predicate. Instead, you use RegisterGeneric. This method allows making a mapping between a generic implementation and its Abstraction, similar to what you saw with RegisterGenericDecorator. To get the sequence of named registrations to be injected into the Composite’s constructor argument, you can once more use the versatile WithParameter method:
builder.RegisterGeneric(typeof(CompositeEventHandler<>))
.As(typeof(IEventHandler<>))
.WithParameter(
(p, c) => true,
(p, c) => c.ResolveNamed("handler", p.ParameterType));
Because CompositeEventHandler<TEvent> contains a single constructor parameter, you simplify the registration to apply to all parameters by letting the predicate return true.
The WithParameter delegates are called when a closed IEventHandler<TEvent> is requested. Therefore, at the time of invocation, you can get the type of the constructor parameter by calling p.ParameterType. For example, if an IEventHandler<OrderApproved> is requested, the parameter type will be IEnumerable<IEventHandler<OrderApproved>>. By passing this type on to the ResolveNamed method with the sequence name handler, Autofac resolves the previously registered sequence of named instances that implement IEventHandler<OrderApproved>.
Although the registration of Decorators is simple, this unfortunately doesn’t hold for Composites. Autofac hasn’t been designed — yet — with the Composite design pattern in mind. It’s likely this will change in a future version.
我们对 Autofac DI 容器的讨论到此结束。在下一章中,我们将把注意力转向 Simple Injector。
This completes our discussion of the Autofac DI Container. In the next chapter, we’ll turn our attention to Simple Injector.
概括
Summary
Autofac DI Container提供了相当全面的 API 并解决了您在使用DI Containers时通常遇到的许多棘手情况。
The Autofac DI Container offers a fairly comprehensive API and addresses many of the trickier situations you typically encounter when you use DI Containers.
An important overall theme for Autofac seems to be one of explicitness. It doesn’t attempt to guess what you mean, but rather offers an easy-to-use API that provides you with options to explicitly enable features.
Autofac enforces stricter separation of concerns between configuring and consuming a container. You configure components using a ContainerBuilder instance, but a ContainerBuilder can’t resolve components. When you’re done configuring a ContainerBuilder, you use it to build an IContainer that you can use to resolve components.
With Autofac, resolving from the root container directly is a bad practice. This can easily lead to memory leaks or concurrency bugs. Instead, you should always resolve from a lifetime scope.
Autofac supports the standard Lifestyles: Transient, Singleton, and Scoped.
Autofac 通过提供允许提供代码块的 API 允许使用不明确的构造函数和类型。这允许创建要执行的服务的任何代码。
Autofac allows working with ambiguous constructors and types by providing an API that allows supplying code blocks. This allows any code that creates a service to be executed.
14
简单注入器 DI 容器
14
The Simple Injector DI Container
在这一章当中
In this chapter
使用 Simple Injector 的基本注册 API
Working with Simple Injector’s basic registration API
In the previous chapter, we looked at the Autofac DI Container, created by Nicholas Blumhardt in 2007. Three years later, Steven created Simple Injector, which we’ll examine in this chapter. We’ll give Simple Injector the same treatment that we gave Autofac in the last chapter. You’ll see how you can use Simple Injector to apply the principles and patterns presented in parts 1–3.
This chapter is divided into four sections. You can read each section independently, though the first section is a prerequisite for the other sections, and the fourth section relies on some methods and classes introduced in the third section. You can read this chapter apart from the rest of the chapters in part 4, specifically to learn about Simple Injector, or you can read it together with the other chapters to compare DI Containers.
Although this chapter isn’t a complete treatment of the Simple Injector container, it gives enough information that you can start using it. This chapter includes information on how to deal with the most common questions that may come up as you use Simple Injector. For more information about this container, see the Simple Injector home page at https://simpleinjector.org.
In this section, you’ll learn where to get Simple Injector, what you get, and how to start using it. We’ll also look at common configuration options. Table 14.1 provides fundamental information that you’re likely to need to get started.
在高层次上,使用 Simple Injector 与使用其他DI Containers没有什么不同。与 Autofac DI 容器(第 13 章介绍)和 Microsoft.Extensions.DependencyInjection DI 容器(第 15 章介绍)一样,使用过程分为两步,如图 14.1所示。
At a high level, using Simple Injector isn’t that different from using the other DI Containers. As with the Autofac DI Container (covered in chapter 13) and the Microsoft.Extensions.DependencyInjection DI Container (covered in chapter 15), usage is a two-step process, as figure 14.1 illustrates.
Figure 14.1 The pattern for using Simple Injector. First, you configure a Container, and then, using the same container instance, you resolve components from it.
As you might remember from chapter 13, to facilitate the two-step process, Autofac uses a ContainerBuilder class that produces an IContainer. Simple Injector, on the other hand, integrates both registration and resolution in the same Container instance. Still, it forces the registration to be a two-step process by disallowing any explicit registrations to be made after the first service is resolved.
Although resolution isn’t that different, Simple Injector’s registration API does differ quite a lot from how most DI Containers work. In its design and implementation, it eliminates many pitfalls that are a common cause of bugs. We’ve discussed most of these pitfalls throughout the book, so in this chapter, we’ll discuss the following differences between Simple Injector and other DI Containers:
范围是环境的,允许对象图始终从容器本身解析,以防止内存和并发错误。
Scopes are ambient, allowing object graphs to always be resolved from the container itself to prevent memory and concurrency bugs.
序列通过不同的 API 注册,以防止意外的重复注册相互覆盖。
Sequences are registered through a different API to prevent accidental duplicate registrations from overriding each other.
不能直接注册原始类型以防止注册变得不明确。
Primitive types can’t be registered directly to prevent registrations from becoming ambiguous.
可以验证对象图以发现常见的配置错误,例如Captive Dependencies。
Object graphs can be verified to spot common configuration errors, such as Captive Dependencies.
完成本节后,您应该对 Simple Injector 的整体使用模式有一个良好的感觉,并且您应该能够在行为良好的场景中开始使用它——所有组件都遵循适当的 DI 模式,例如构造函数注射。让我们从最简单的场景开始,看看如何使用 Simple Injector 容器解析对象。
When you’re done with this section, you should have a good feeling for the overall usage pattern of Simple Injector, and you should be able to start using it in well-behaved scenarios — where all components follow proper DI patterns, such as Constructor Injection. Let’s start with the simplest scenario and see how you can resolve objects using a Simple Injector container.
The core service of any DI Container is to compose object graphs. In this section, we’ll look at the API that lets you compose object graphs with Simple Injector.
If you remember the discussion about resolving components with Autofac, you may recall that Autofac requires you to register all relevant components before you can resolve them. This isn’t the case with Simple Injector; if you request a concrete type with a parameterless constructor, no configuration is necessary. The following listing shows one of the simplest possible uses of Simple Injector.
Given an instance of SimpleInjector.Container, you can use the generic GetInstance method to get an instance of the concrete SauceBéarnaise class. Because this class has a parameterless constructor, Simple Injector automatically creates an instance of it. No explicit configuration of the container is necessary.
As you learned in section 12.1.2, Auto-Wiring is the ability to automatically compose an object graph by making use of the type information. Because Simple Injector supports Auto-Wiring, even in the absence of a parameterless constructor, it can create instances without configurations as long as the involved constructor parameters are all concrete types, and all parameters in the entire tree have leaf types with parameterless constructors. As an example, consider this Mayonnaise constructor:
public Mayonnaise(EggYolk eggYolk, SunflowerOil oil)
Although the mayonnaise recipe is a bit simplified, suppose both EggYolk and SunflowerOil are concrete classes with parameterless constructors. Although Mayonnaise itself has no parameterless constructor, Simple Injector creates it without any configuration:
var container = new Container();
Mayonnaise mayo = container.GetInstance<Mayonnaise>();
This works because Simple Injector is able to figure out how to create all required constructor parameters. But as soon as you introduce loose coupling, you must configure Simple Injector by mapping Abstractions to concrete types.
Although Simple Injector’s ability to Auto-Wire concrete types certainly can come in handy from time to time, loose coupling requires you to map Abstractions to concrete types. Creating instances based on such maps is the core service offered by any DI Container, but you must still define the map. In this example, you map the IIngredient interface to the concrete SauceBéarnaise class, which allows you to successfully resolve IIngredient:
var container = new Container();
container.Register<IIngredient, SauceBéarnaise>(); ①
IIngredient sauce =
container.GetInstance<IIngredient>(); ②
You use the Container instance to register types and define maps. Here, the generic Register method allows an Abstraction to be mapped to a particular implementation. This lets you register a concrete type. Because of the previous Register call, SauceBéarnaise can now be resolved as IIngredient.
在许多情况下,通用 API 就是您所需要的。不过,在某些情况下,您需要一种更弱类型的方式来解析服务。这也是可能的。
In many cases, the generic API is all you need. Still, there are situations where you need a more weakly typed way to resolve services. This is also possible.
解决弱类型服务
Resolving weakly typed services
有时您不能使用通用 API,因为您在设计时不知道合适的类型。您只有一个Type实例,但您仍然希望获得该类型的实例。您在第 7.3 节中看到了一个示例,其中我们讨论了 ASP.NET Core MVC 的IControllerActivator类. 相关的方法是这个:
Sometimes you can’t use a generic API, because you don’t know the appropriate type at design time. All you have is a Type instance, but you’d still like to get an instance of that type. You saw an example of that in section 7.3, where we discussed ASP.NET Core MVC’s IControllerActivator class. The relevant method is this one:
As shown previously in listing 7.8, the ControllerContext captures the controller’s Type, which you can extract using the ControllerTypeInfo property of the ActionDescriptor property:
Type controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType();
Because you only have a Type instance, you can’t use the generic GetInstance<T> method, but must resort to a weakly typed API. Simple Injector offers a weakly typed overload of the GetInstance method that lets you implement the Create method like this:
Type controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType();
return container.GetInstance(controllerType);
的弱类型重载GetInstance允许您传递controllerType变量直接到 Simple Injector。通常,这意味着您必须将返回值转换为某种抽象,因为弱类型GetInstance方法会返回object. 但是,在 的情况下IControllerActivator,这不是必需的,因为 ASP.NET Core MVC 不需要控制器来实现任何接口或基类。
The weakly typed overload of GetInstance lets you pass the controllerType variable directly to Simple Injector. Typically, this means you have to cast the returned value to some Abstraction because the weakly typed GetInstance method returns object. In the case of IControllerActivator, however, this isn’t required, because ASP.NET Core MVC doesn’t require controllers to implement any interface or base class.
No matter which overload of GetInstance you use, Simple Injector guarantees that it’ll return an instance of the requested type or throw an exception if there are Dependencies that can’t be satisfied. When all required Dependencies have been properly configured, Simple Injector can Auto-Wire the requested type.
To be able to resolve the requested type, all loosely coupled Dependencies must have been previously configured. You can configure Simple Injector in many ways; the next section reviews the most common ones.
As we discussed in section 12.2, you can configure a DI Container in several conceptually different ways. Figure 12.5 reviewed the options: configuration files, Configuration as Code, and Auto-Registration. Figure 14.2 shows these options again.
Figure 14.2 The most common ways to configure a DI Container shown against dimensions of explicitness and the degree of binding.
Simple Injector 的核心配置 API 以代码为中心,同时支持Configuration as Code和基于约定的Auto-Registration。基于文件的配置完全被排除在外。这不应该成为使用 Simple Injector 的障碍,因为正如我们在第 12 章中讨论的那样,通常应该避免使用这种配置方法。尽管如此,如果您的应用程序需要后期绑定,您自己添加基于文件的配置还是很容易的,我们将在本节后面讨论。
Simple Injector’s core configuration API is centered on code and supports both Configuration as Code and convention-based Auto-Registration. File-based configuration is left out completely. This shouldn’t be a obstacle to using Simple Injector because, as we discussed in chapter 12, this configuration method should generally be avoided. Still, if your application requires late binding, it’s quite easy to add a file-based configuration yourself, as we’ll discuss later in this section.
All configuration in Simple Injector uses the API exposed by the Container class. One of the most commonly used methods is the Register method that you’ve already seen:
Because you want to program to interfaces, most of your components will depend on Abstractions. This means that most components will be registered by their corresponding Abstraction. When a component is the topmost type in the object graph, its not uncommon to resolve it by its concrete type instead of its Abstraction. MVC controllers, for instance, are resolved by their concrete type.
In general, you would register a type either by its Abstraction or by its concrete type, but not both. There are exceptions to this rule, however. In Simple Injector, registering a component both by its concrete type and its Abstraction is simply a matter of adding an extra registration:
Instead of registering the class only as IIngredient, you can register it as both itself and the interface it implements. This enables the container to resolve requests for both SauceBéarnaise and IIngredient.
In real applications, you always have more than one Abstraction to map, so you must configure multiple mappings. This is done with multiple calls to Register:
This example maps IIngredient to SauceBéarnaise, and ICourse to Course. There’s no overlap of types, so it should be pretty evident what’s going on. But what would happen if you register the same Abstraction several times?
container.Register<IIngredient, SauceBéarnaise>();
container.Register<IIngredient, Steak>(); ①
在这里,您注册IIngredient了两次,这导致在第二行抛出异常并显示以下消息:
Here, you register IIngredient twice, which results in an exception being thrown on the second line with the following message:
Type IIngredient has already been registered. If your intention is to resolve a collection of IIngredient implementations, use the Collection.Register overloads. For more information, see https://simpleinjector.org/coll1.
与大多数其他DI 容器相比,Simple Injector 不允许堆叠注册来构建类型序列,如前面的代码片段所示。它的 API 明确地将序列的注册与单个抽象映射分开。1 而不是多次调用RegisterSimple Injector 强制您使用Collection属性的注册方法,例如Collection.Register:
In contrast to most other DI Containers, Simple Injector doesn’t allow stacking up registrations to build up a sequence of types, as the previous code snippet shows. Its API explicitly separates the registration of sequences from single Abstraction mappings.1 Instead of making multiple calls to Register, Simple Injector forces you to use the registration methods of the Collection property, such as Collection.Register:
This example registers all ingredients in one single call. Alternatively, you can use Collection.Append to add implementations to a sequence of ingredients:
With the previous registrations, any component that depends on IEnumerable<IIngredient> gets a sequence of ingredients injected. Simple Injector handles multiple configurations for the same Abstraction well, but we’ll get back to this topic in section 14.4.
Although there are more-advanced options available for configuring Simple Injector, you can configure an entire application with the methods shown here. But to save yourself from too much explicit maintenance of container configuration, you could instead consider a more convention-based approach using Auto-Registration.
In many cases, registrations will be similar. Such registrations are tedious to maintain, and explicitly registering each and every component might not be the most productive approach, as we discussed in section 12.3.3.
Consider a library that contains many IIngredient implementations. You can configure each class individually, but it’ll result in an ever-changing list of Type instances supplied to the Collection.Register method. What’s worse is that, every time you add a new IIngredient implementation, you must also explicitly register it with the Container if you want it to be available. It’d be more productive to state that all implementations of IIngredient found in a given assembly should be registered.
This is possible using some of the Register and Collection.Register method overloads. These particular overloads let you specify an assembly and configure all selected classes from this assembly in a single statement. To get the Assembly instance, you can use a representative class; in this case, Steak:
The previous example unconditionally configures all implementations of the IIngredient interface, but you can provide filters that enable you to select only a subset. Here’s a convention-based scan where you add only classes whose name starts with Sauce:
This scan makes use of the GetTypesToRegister method, which searches for types without registering them. This allows you to filter the selection using a predicate. Instead of supplying Collection.Register using a list of assemblies, you now supply it with a list of Type instances.
Apart from selecting the correct types from an assembly, another part of Auto-Registration is defining the correct mapping. In the previous examples, you used the Collection.Register method with a specific interface to register all selected types against that interface. Sometimes, however, you may want to use different conventions. Let’s say that instead of interfaces, you use abstract base classes, and you want to register all types in an assembly where the name ends with Policy by their base type:
Assembly policiesAssembly = typeof(DiscountPolicy).Assembly;
var policyTypes =
from type in policiesAssembly.GetTypes() ① where type.Name.EndsWith("Policy") ②
select type;
foreach (Type type in policyTypes)
{
container.Register(type.BaseType, type); ③
}
在此示例中,您几乎不使用 Simple Injector API 的任何部分。相反,您可以使用 .NET 框架提供的反射和 LINQ API 来过滤和获取预期的类型。
In this example, you hardly use any part of the Simple Injector API. Instead, you use the reflection and LINQ APIs provided by the .NET framework to filter and get the expected types.
尽管 Simple Injector 基于约定的 API 有限,但通过使用现有的 .NET 框架 API,基于约定的注册仍然非常容易。Simple Injector 基于约定的 API 主要关注序列和泛型类型的注册。当谈到泛型,这就是为什么 Simple Injector 明确支持基于泛型抽象注册类型的原因,我们将在接下来讨论。
Even though Simple Injector’s convention-based API is limited, by making use of existing .NET framework APIs, convention-based registrations are still surprisingly easy. The Simple Injector’s convention-based API mainly focuses around the registration of sequences and generic types. This becomes a different ball game when it comes to generics, which is why Simple Injector has explicit support for registering types based on generic Abstractions, as we’ll discuss next.
During the course of chapter 10, you refactored the big, obnoxious IProductService interface to the ICommandService<TCommand> interface of listing 10.12. Here’s that Abstraction again:
public interface ICommandService<TCommand>
{
void Execute(TCommand command);
}
As discussed in chapter 10, every command Parameter Object represents a use case, and there’ll be a single implementation per use case. The AdjustInventoryService of listing 10.8 was given as an example. It implemented the “adjust inventory” use case. The next listing shows this class again.
Any reasonably complex system will easily implement hundreds of use cases, and these are ideal candidates for using Auto-Registration. With Simple Injector, this couldn’t be simpler.
In contrast to the previous listing that used Collection.Register, you again make use of Register. This is because there’ll always be exactly one implementation of a requested command service; you don’t want to inject a sequence of command services.
Using the supplied open-generic interface, Simple Injector iterates through the list of assembly types and registers types that implement a closed-generic version of ICommandService<TCommand>. What this means, for instance, is that AdjustInventoryService is registered because it implements ICommandService<AdjustInventory>, which is a closed-generic version of ICommandService<TCommand>.
Not all ICommandService<TCommand> implementations will be registered, though. Simple Injector skips open-generic implementations, Decorators, and Composites, as they often require special registration. We’ll discuss this in section 14.4.
The Register method takes a params array of Assembly instances, so you can supply as many assemblies as you like to a single convention. It’s not a far-fetched idea to scan a folder for assemblies and supply them all to implement add-in functionality where add-ins can be added without recompiling a core application. (For an example, see https://simpleinjector.org/registering-plugins-dynamically.) This is one way to implement late binding; another is to use configuration files.
使用配置文件配置容器
Configuring the container using configuration files
When you need to be able to change a configuration without recompiling the application, configuration files are a good option. The most natural way to use configuration files is to embed them into a standard .NET application configuration file. This is possible, but you can also use a standalone configuration file if you need to be able to vary the Simple Injector configuration independently of the standard .config file.
As stated in the beginning of this section, there’s no explicit support in Simple Injector for file-based configuration. By making use of .NET Core’s built-in configuration system, however, loading registrations from a configuration file is rather straightforward. For this purpose, you can define your own configuration structure that maps Abstractions to implementations. Here’s a simple example that maps the IIngredient interface to the Steak class.
The registrations element is a JSON array of registration elements. The previous example contained a single registration, but you can add as many registration elements as you like. In each element, you must specify a concrete type with the implementation attribute. To map the Steak class to IIngredient, you can use the service attribute.
Using .NET Core’s built-in configuration system, you can load a configuration file and iterate through it. You then append the defined registrations to the container:
var config = new ConfigurationBuilder() ① .AddJsonFile("simpleinjector.json") ① .Build(); ① var registrations = config ② .GetSection("registrations").GetChildren(); ② foreach (var reg in registrations) ③ { ③ container.Register( ③ Type.GetType(reg["service"]), ③ Type.GetType(reg["implementation"])); ③
}
A configuration file is a good option when you need to change the configuration of one or more components without recompiling the application, but because it tends to be quite brittle, you should reserve it for only those occasions and use either Auto-Registration or Configuration as Code for the main part of the container’s configuration.
本节介绍了 Simple Injector DI 容器并演示了这些基本机制:如何配置Container,以及随后如何使用它来解析服务。只需调用一次GetInstance方法即可轻松完成解析服务,因此复杂性涉及配置容器。这可以通过几种不同的方式完成,包括命令式代码和配置文件。
This section introduced the Simple Injector DI Container and demonstrated these fundamental mechanics: how to configure a Container, and, subsequently, how to use it to resolve services. Resolving services is easily done with a single call to the GetInstance method, so the complexity involves configuring the container. This can be done in several different ways, including imperative code and configuration files.
Until now, we’ve only looked at the most basic API; we have yet to cover more-advanced areas. One of the most important topics is how to manage component lifetime.
14.2 管理生命周期
14.2 Managing lifetime
在第 8 章中,我们讨论了生命周期管理,包括最常见的概念生命周期,例如Transient、Singleton和Scoped。Simple Injector 的Lifestyle支持映射到这三种Lifestyles。表 14.2中显示的生活方式作为 API 的一部分提供。
In chapter 8, we discussed Lifetime Management, including the most common conceptual Lifestyles such as Transient, Singleton, and Scoped. Simple Injector’s Lifestyle supports mapping to these three Lifestyles. The Lifestyles shown in table 14.2 are available as part of the API.
Simple Injector’s implementations of Transient and Singleton are equivalent to the general Lifestyles described in chapter 8, so we won’t spend much time on them in this chapter. Instead, in this section, you’ll see how you can define Lifestyles for components in code. We’ll also look at Simple Injector’s concept of ambient scoping and how it can simplify working with the container. We’ll then cover how Simple Injector can verify and diagnose its configuration to prevent common configuration errors. By the end of this section, you should be able to use Simple Injector’s Lifestyles in your own application. Let’s start by reviewing how to configure Lifestyles for components.
In this section, we’ll review how to manage Lifestyles with Simple Injector. A Lifestyle is configured as part of registering components. It’s as easy as this:
This example configures the concrete SauceBéarnaise class as a Singleton so that the same instance is returned each time SauceBéarnaise is requested. If you want to map an Abstraction to a concrete class with a specific lifetime, you can use the usual Register overload with two generic arguments, while supplying it with the Lifestyle.Singleton:
Configuring Lifestyles for convention-based registrations can be done in several ways. When registering a sequence, for instance, one of the options is to supply the Collection.Register method with a list of Registration instances:
Assembly assembly = typeof(Steak).Assembly;
var types = container.GetTypesToRegister<IIngredient>(assembly);
container.Collection.Register<IIngredient>(
from type in types
select Lifestyle.Singleton.CreateRegistration(type, container));
You can use Lifestyle.Singleton to define the Lifestyle for all registrations in a convention. In this example, you define all IIngredient registrations as Singleton by supplying them all as a Registration instance to the Collection.Register overload.3
When it comes to configuring Lifestyles for components, there are many options. In all cases, it’s done in a rather declarative fashion. Although configuration is typically easy, you mustn’t forget that some Lifestyles involve long-lived objects, which use resources as long as they’re around.
As discussed in section 8.2.2, it’s important to release objects when you’re done with them. Similar to Autofac, Simple Injector has no explicit Release method, but instead uses a concept called scopes. A scope can be regarded as a request-specific cache. As figure 14.3 illustrates, it defines a boundary where components can be reused.
Figure 14.3 Simple Injector’s Scope acts as a request-specific cache that can share components for a limited duration or purpose.
AScope定义了一个缓存,您可以将其用于特定的持续时间或目的;最明显的例子是网络请求。当从 a 请求范围内的组件时Scope,您总是收到相同的实例。与真正的单例的不同之处在于,如果您查询第二个范围,您将获得另一个实例。
A Scope defines a cache that you can use for a particular duration or purpose; the most obvious example is a web request. When a scoped component is requested from a Scope, you always receive the same instance. The difference from true Singletons is that if you query a second scope, you’ll get another instance.
One of the important features of scopes is that they let you properly release components when the scope completes. You create a new scope with the BeginScope method of a particular ScopedLifestyle implementation and release all appropriate components by invoking its Dispose method:
using (AsyncScopedLifestyle.BeginScope(container)) ①
{
IMeal meal = container.GetInstance<IMeal>(); ② meal.Consume(); ③ } ④
This example shows how IMeal is resolved from the Container instance, instead of being resolved from a Scope instance. This isn’t a typo — the container automatically “knows” in which active scope it’s operating. The next section discusses this in more detail.
In the previous example, a new scope is created by invoking the BeginScope method on the corresponding Scoped Lifestyle. The return value implements IDisposable, so you can wrap it in a using block.
When you’re done with a scope, you can dispose of it with a using block. This happens automatically when you exit the block, but you can also choose to explicitly dispose of it by invoking the Dispose method. When you dispose of a scope, you also release all the components that were created during that scope. In the example, it means that you release the meal object graph.
Earlier in this section, you saw how to configure components as Singletons or Transients. Configuring a component to have its Lifestyle tied to a scope is done in a similar way:
Similar to Lifestyle.Singleton and Lifestyle.Transient, you can use the Lifestyle.Scoped value to state that the component’s lifetime should live for the duration of the scope that created the instance. This call by itself, however, would cause the container to throw the following exception:
To be able to use the Lifestyle.Scoped property, please ensure that the container is configured with a default scoped lifestyle by setting the Container.Options.DefaultScopedLifestyle property with the required scoped lifestyle for your type of application. For more information, see https://simpleinjector.org/scoped.
在您可以使用该Lifestyle.Scoped值之前,Simple Injector 要求您设置该Container.Options.DefaultScopedLifestyle属性。Simple Injector 有多个ScopedLifestyle实现,有时特定于一个框架。这意味着您必须明确配置ScopedLifestyle最适合您的应用程序类型的实现。对于 ASP.NET Core 应用程序,正确的Scoped Lifestyle是AsyncScopedLifestyle,您可以像这样配置它:
Before you can use the Lifestyle.Scoped value, Simple Injector requires that you set the Container.Options.DefaultScopedLifestyle property. Simple Injector has multiple ScopedLifestyle implementations that are sometimes specific to a framework. This means you’ll have to explicitly configure the ScopedLifestyle implementation that works best for your type of application. For ASP.NET Core applications, the proper Scoped Lifestyle is the AsyncScopedLifestyle, which you can configure like this:
var container = new Container();
container.Options.DefaultScopedLifestyle = ① new AsyncScopedLifestyle(); ①
container.Register<IIngredient, SauceBéarnaise>(
Lifestyle.Scoped); ②
Due to their nature, Singletons are never released for the lifetime of the container itself. Still, you can release even those components if you don’t need the container any longer. This is done by disposing of the container itself:
In practice, this isn’t nearly as important as disposing of a scope, because the lifetime of a container tends to correlate closely with the lifetime of the application it supports. You normally keep the container around as long as the application runs, so you only dispose of it when the application shuts down. In this case, memory would be reclaimed by the operating system.
As we mentioned earlier in this section, with Simple Injector, you always resolve objects from the container — not from a scope. This works because scopes are ambient in Simple injector. Let’s look at ambient scopes next.
With Simple Injector, the previous example of the creation and disposal of the scope shows how you can always resolve instances from the Container, even if you resolve scoped instances. The following example shows this again:
using (AsyncScopedLifestyle.BeginScope(container))
{
IMeal meal = container.GetInstance<IMeal>(); ①
meal.Consume();
}
This reveals an interesting feature of Simple Injector, which is that scope instances are ambient and are globally available in the context in which they’re running. The following listing shows this behavior.
var container = new Container();
container.Options.DefaultScopedLifestyle =
new AsyncScopedLifestyle();
Scope scope1 = Lifestyle.Scoped ① .GetCurrentScope(container); ①
using (Scope scope2 =
AsyncScopedLifestyle.BeginScope(container))
{
Scope scope3 = Lifestyle.Scoped ② .GetCurrentScope(container); ②
}
Scope scope4 = Lifestyle.Scoped ③ .GetCurrentScope(container); ③
This behavior is similar to that of .NET’s TransactionScope class.4 When you wrap an operation with a TransactionScope, all database connections opened within that operation will automatically be part of the same transaction.
In general, you won’t use the GetCurrentScope method a lot, if at all. The Container uses this under the hood on your behalf when you start resolving instances. Still, it demonstrates nicely that Scope instances can be retrieved and are accessible from the container.
A ScopedLifestyle implementation, such as the previous AsyncScopedLifestyle, stores its created Scope instance for later use, which allows it to be retrieved within the same context. It’s the particular ScopedLifestyle implementation that defines when code runs in the same context. The AsyncScopedLifestyle, for instance, stores the Scope internally in an System.Threading.AsyncLocal<T>.5 This allows scopes to flow from method to method, even if an asynchronous method continues on a different thread, as this example demonstrates:
using (AsyncScopedLifestyle.BeginScope(container))
{
IMeal meal = container.GetInstance<IMeal>();
await meal.Consume(); ① meal = container.GetInstance<IMeal>(); ②
}
Although ambient scopes might be confusing at first, their usage typically simplifies working with Simple Injector. For instance, you won’t have to worry about getting memory leaks when resolving from the container, because Simple Injector manages this transparently on your behalf. Scope instances will never be cached in the root container, which is something you need to be cautious about with the other containers described in this book. Another area in which Simple Injector excels is the ability to detect common misconfigurations.
14.2.4 诊断容器的常见生命周期问题
14.2.4 Diagnosing the container for common lifetime problems
Compared to Pure DI, registration and building object graphs in a DI Container is more implicit. This makes it easy to accidentally misconfigure the container. For that reason, many DI Containers have a function that allows all registrations to be iterated to enable verifying whether all can be resolved, and Simple Injector is no exception.
Being able to resolve an object graph, however, is no guarantee of the correctness of the configuration, as the Captive Dependency pitfall of section 8.4.1 illustrates. A Captive Dependency is a misconfiguration of the lifetime of a component. In fact, most errors concerning working with DI Containers are related to lifetime misconfigurations.
Because DI Container misconfigurations are so common and often difficult to trace, Simple Injector lets you verify its configuration, which goes beyond the simple instantiation of object graphs that most DI Containers support. On top of that, Simple Injector scans the object graphs for common misconfigurations — Captive Dependencies being one of them.
Therefore, the configure step of Simple Injector’s two-step process, as outlined in figure 14.1, exists of two substeps. Figure 14.4 shows this process.
The easiest way to let Simple Injector diagnose and detect configuration errors is by calling the Container’s Verify method, as shown in the following listing.
The Captive Dependency misconfiguration is one that Simple Injector detects. Now let’s see how you can cause Verify to trip on a Captive Dependency using the Mayonnaise ingredient of section 14.1.1. Its constructor contained two Dependencies:
public Mayonnaise(EggYolk eggYolk, SunflowerOil oil)
The following listing registers Mayonnaise with its two Dependencies. But it misconfigures Mayonnaise as Singleton, whereas its EggYolkDependency is registered as Transient.
Listing 14.7 Causing the container to detect a Captive Dependency
var container = new Container();
container.Register<EggYolk>(Lifestyle.Transient); ① container.Register<Mayonnaise>(Lifestyle.Singleton); ②
container.Register<SunflowerOil>(Lifestyle.Singleton);
container.Verify(); ③
When you call Register, Simple Injector only performs some rudimentary validations. This includes checking that the type isn’t abstract, that it has a public constructor, and the like. It won’t check for problems such as Captive Dependencies at that stage, because registrations can be made in any arbitrary order. In listing 14.7, for instance, SunflowerOil is registered after Mayonnaise, even though it’s a Dependency of Mayonnaise. It’s completely valid to do so. It’s only after the configuration is completed that verification can be performed. When you run this code example, the call to Verify fails with the following exception message:
配置无效。报告了以下诊断警告:
The configuration is invalid. The following diagnostic warnings were reported:
-[Lifestyle Mismatch] Mayonnaise (Singleton) depends on EggYolk (Transient). See the Error property for detailed information about the warnings. Please see https://simpleinjector.org/diagnostics how to fix problems and how to suppress individual warnings.
An interesting observation here is that Simple Injector doesn’t allow Transient Dependencies to be injected into Singleton consumers. This is the opposite of Autofac. With Autofac, Transients are implicitly expected to live as long as their consumer, which means that in Autofac, this situation is never considered to be a Captive Dependency. For that reason, Autofac calls a TransientInstancePerDependency, which pretty much describes its behavior: each consumer’s Dependency that’s configured as Transient is expected to get its own instance. Because of that, Autofac only detects the injection of scoped components into Singletons as Captive Dependencies.
Although this might sometimes be exactly the behavior you need, in most cases, it’s not. More often, Transient components are expected to live for a brief period of time, whereas injecting them into a Singleton consumer causes the component to live for as long as the application lives. Because of this, Simple Injector’s motto is: “better safe than sorry,” which is why it throws an exception. Sometimes you might need to suppress such warnings in cases where you know best.
var container = new Container();
Registration reg = Lifestyle.Transient ① .CreateRegistration<EggYolk>(container); ① reg.SuppressDiagnosticWarning( ② DiagnosticType.LifestyleMismatch, ② justification: "I like to eat rotten eggs."); ② container.AddRegistration(typeof(EggYolk), reg); ③
container.Register<Mayonnaise>(Lifestyle.Singleton);
container.Register<SunflowerOil>(Lifestyle.Singleton);
container.Verify();
SuppressDiagnosticWarning contains a required justification argument. It isn’t used by SuppressDiagnosticWarning at all, but serves as a reminder so that you don’t forget to document why the warning is suppressed.
This completes our tour of Lifetime Management with Simple Injector. Components can be configured with mixed Lifestyles, and this is even true when you register multiple implementations of the same Abstraction.
Until now, you’ve allowed the container to wire Dependencies by implicitly assuming that all components use Constructor Injection. But this isn’t always the case. In the next section, we’ll review how to deal with classes that must be instantiated in special ways.
14.3 注册困难的 API
14.3 Registering difficult APIs
到目前为止,我们已经考虑了如何配置使用构造函数注入的组件。构造函数注入的众多好处之一是像 Simple Injector 这样的DI 容器可以轻松理解如何在依赖图中组合和创建所有类。当 API 表现不佳时,这一点就不太清楚了。
Until now, we’ve considered how you can configure components that use Constructor Injection. One of the many benefits of Constructor Injection is that DI Containers like Simple Injector can easily understand how to compose and create all classes in a Dependency graph. This becomes less clear when APIs are less well behaved.
In this section, you’ll see how to deal with primitive constructor arguments and static factories. These all require special attention. Let’s start by looking at classes that take primitive types, such as strings or integers, as constructor arguments.
As long as you inject Abstractions into consumers, all is well. But it becomes more difficult when a constructor depends on a primitive type, such as a string, a number, or an enum. This is particularly the case for data access implementations that take a connection string as constructor parameter, but it’s a more general issue that applies to all string and numeric types.
Conceptually, it doesn’t make sense to register a string or number as a component in a container. In particular, when Auto-Wiring is used, the registration of primitive types causes ambiguity. Take string, for instance. Where one component might require a database connection string, another might require a file path. The two are conceptually different, but because Auto-Wiring works by selecting Dependencies based on their type, they become ambiguous. For that reason, Simple Injector blocks the registration of primitive Dependencies. Consider as an example this constructor:
public ChiliConCarne(Spiciness spiciness)
在这个例子中,Spiciness是一个枚举:
In this example, Spiciness is an enum:
public enum Spiciness { Mild, Medium, Hot }
您可能想像ChiliConCarne下面的示例那样进行注册。那行不通的!
You might be tempted to register ChiliConCarne as in the following example. That won’t work!
container.Register<ICourse, ChiliConCarne>();
此行导致异常并显示以下消息:
This line causes an exception with the following message:
The constructor of type ChiliConCarne contains parameter 'spiciness' of type Spiciness, which cannot be used for constructor injection because it’s a value type.
The downside of using delegates is that the registration has to be changed when the ChiliConCarne constructor changes. When you add an IIngredientDependency to the ChiliConCarne constructor, for instance, the registration must be updated:
container.Register<ICourse>(() => ①
new ChiliConCarne(
Spiciness.Medium,
container.GetInstance<IIngredient>())); ②
Besides the additional maintenance in the Composition Root, and because of the lack of Auto-Wiring, the use of delegates disallows Simple Injector from verifying the validity of the relationship between ChiliConCarne and its IIngredientDependency. The delegate hides the fact that this Dependency exists. This isn’t always a problem, but it can complicate diagnosing problems that are caused due to misconfigurations. Because of these downsides, a more convenient solution is to extract the primitive Dependencies into Parameter Objects.
14.3.2 提取对参数对象的原始依赖
14.3.2 Extracting primitive Dependencies to Parameter Objects
In section 10.3.3, we discussed how the introduction of Parameter Objects allowed mitigating the Open/Closed Principle violation that IProductService caused. Parameter Objects, however, are also a great tool to mitigate ambiguity. For example, the Spiciness of a course could be described in more general terms as a flavoring. Flavoring might include other properties, such as saltiness, so you can wrap Spiciness and the saltiness in a Flavoring class:
public class Flavoring
{
public readonly Spiciness Spiciness;
public readonly bool ExtraSalty;
public Flavoring(Spiciness spiciness, bool extraSalty)
{
this.Spiciness = spiciness;
this.ExtraSalty = extraSalty;
}
}
As we mentioned in section 10.3.3, it’s perfectly fine for Parameter Objects to have one parameter. The goal is to remove ambiguity, and not just on the technical level. Such a Parameter Object’s name might do a better job describing what your code does on a functional level, as the Flavoring class so elegantly does. With the introduction of the Flavoring Parameter Object, it now becomes possible to Auto-Wire any ICourse implementation that requires some flavoring:
var flavoring = new Flavoring(Spiciness.Medium, extraSalty: true);
container.RegisterInstance<Flavoring>(flavoring);
container.Register<ICourse, ChiliConCarne>();
This code creates a single instance of the Flavoring class. Flavoring becomes a configuration object for courses. Because there’ll only be one Flavoring instance, you can register it in Simple Injector using RegisterInstance.
Extracting primitive Dependencies into Parameter Objects should be your preference over the previously discussed option, because Parameter Objects remove ambiguity, at both the functional and technical levels. It does, however, require a change to a component’s constructor, which might not always be feasible. In this case, registering a delegate is your second-best pick.
As we discussed in the previous section, one of the options for creating a component with a primitive value is to use the Register method. This lets you supply a delegate that creates the component. Here’s that registration again:
container.Register<ICourse>(() => new ChiliConCarne(Spiciness.Hot));
The ChiliConCarne constructor is invoked with Hot Spiciness every time the ICourse service is resolved. Instead of Simple Injector figuring out the constructor arguments, however, you write the constructor invocation yourself using a code block.
When it comes to application classes, you typically have a choice between Auto-Wiring or using a code block. But other classes are more restrictive: they can’t be instantiated through a public constructor. Instead, you must use some sort of factory to create instances of the type. This is always troublesome for DI Containers because, by default, they look after public constructors.
考虑公共JunkFood类的这个示例构造函数:
Consider this example constructor for the public JunkFood class:
Even though the JunkFood class might be public, the constructor is internal. In the next example, instances of JunkFood should instead be created through the static JunkFoodFactory class:
public static class JunkFoodFactory
{
public static JunkFood Create(string name)
{
return new JunkFood(name);
}
}
From Simple Injector’s perspective, this is a problematic API, because there are no unambiguous and well-established conventions around static factories. It needs help — and you can give that help by providing a code block it can execute to create the instance:
This time, you use the Register method to create the component by invoking a static factory within the code block. JunkFoodFactory.Create is invoked every time IMeal is resolved, and the result is returned.
When you end up writing the code to create the instance, how is this in any way better than invoking the code directly? By using a code block inside a Register method call, you still gain something:
你映射从IMeal到JunkFood。这允许消费类保持松散耦合。
You map from IMeal to JunkFood. This allows consuming classes to stay loosely coupled.
You can still configure Lifestyles. Although the code block will be invoked to create the instance, it may not be invoked every time the instance is requested. It is by default, but if you change it to a Singleton, the code block will only be invoked once, and the result cached and reused thereafter.
In this section, you’ve seen how you can use Simple Injector to deal with more-difficult APIs. You can use the Register method with a code block for a more type-safe approach. We have yet to look at how to work with multiple components, so let’s now turn our attention in that direction.
As alluded to in section 12.1.2, DI Containers thrive on distinctness but have a hard time with ambiguity. When using Constructor Injection, a single constructor is preferred over overloaded constructors, because it’s evident which constructor to use when there’s no choice. This is also the case when mapping from Abstractions to concrete types. If you attempt to map multiple concrete types to the same Abstraction, you introduce ambiguity.
Despite the undesirable qualities of ambiguity, you often need to work with multiple implementations of a single Abstraction.8 This can be the case in these situations:
不同的混凝土类型用于不同的消费者。
Different concrete types are used for different consumers.
In this section, we’ll look at each of these cases and see how Simple Injector addresses each one in turn. When we’re done, you should be able to register and resolve components even when multiple implementations of the same Abstraction are in play. Let’s first see how you can provide fine-grained control in the case of ambiguity.
Auto-Wiring is convenient and powerful but provides little control. As long as all Abstractions are distinctly mapped to concrete types, you have no problems. But as soon as you introduce more implementations of the same interface, ambiguity rears its ugly head. Let’s first recap how Simple Injector deals with multiple registrations of the same Abstraction.
配置同一服务的多个实现
Configuring multiple implementations of the same service
正如您在 14.1.2 节中看到的,您可以像这样注册同一接口的多个实现:
As you saw in section 14.1.2, you can register multiple implementations of the same interface like this:
This example registers both the Steak and SauceBéarnaise classes as a sequence of IIngredient services. You can ask the container to resolve all IIngredient components. Simple Injector has a dedicated method to do that: GetAllInstances gets an IEnumerable with all registered ingredients. Here’s an example:
Notice that you request IEnumerable<IIngredient>, but you use the normal GetInstance method. Simple Injector interprets this as a convention and gives you all the IIngredient components it has.
When there are multiple implementations of a certain Abstraction, there’ll often be a consumer that depends on a sequence. Sometimes, however, components need to work with a fixed set or a subset of Dependencies of the same Abstraction, which is what we’ll discuss next.
使用条件注册消除歧义
Removing ambiguity using conditional registrations
As useful as Auto-Wiring is, sometimes you need to override the normal behavior to provide fine-grained control over which Dependencies go where, but it may also be that you need to address an ambiguous API. As an example, consider this constructor:
public ThreeCourseMeal(ICourse entrée, ICourse mainCourse, ICourse dessert)
In this case, you have three identically typed Dependencies, each of which represents a different concept. In most cases, you want to map each of the Dependencies to a separate type. With most DI Containers, the typical solution for this type of problem is to use keyed or named registrations, as you saw with Autofac in the previous chapter. With Simple Injector, the solution is typically to change the registration of the Dependency instead of the consumer. The following listing shows how you could choose to register the ICourse mappings.
Listing 14.9 Registering courses based on the constructor’s parameter names
container.Register<IMeal, ThreeCourseMeal>(); ① container.RegisterConditional<ICourse, Rillettes>( ② c => c.Consumer.Target.Name == "entrée"); ② ② container.RegisterConditional<ICourse, CordonBleu>( ② c => c.Consumer.Target.Name == "mainCourse"); ② ② container ② .RegisterConditional<ICourse, MousseAuChocolat>( ② c => c.Consumer.Target.Name == "dessert"); ②
Let’s take a closer look at what’s going on here. The RegisterConditional method accepts a Predicate<PredicateContext> value, which allows it to determine whether a registration should be injected into the consumer or not. It has the following signature:
public void RegisterConditional<TService, TImplementation>(
Predicate<PredicateContext> predicate)
where TImplementation : class, TService
where TService : class;
System.Predicate<T> is a .NET delegate type. The predicate value will be invoked by Simple Injector. If predicate returns true, it uses the registration for the given consumer. Otherwise, Simple Injector expects another conditional registration to have a delegate that returns true. It throws an exception when it can’t find a registration, because, in that case, the object graph can’t be constructed. Likewise, it throws an exception when there are multiple registrations that are applicable.
Simple Injector is strict and never assumes to know what you intended to select, as we discussed previously regarding components with multiple constructors. This does mean, though, that Simple Injector always calls all predicates of all applicable conditional registrations to find possible overlapping registrations. This might seem inefficient, but those predicates are only called when a component is resolved for the first time. Any following resolution has all the information available, which means additional resolutions are fast.
By overriding Auto-Wiring using conditional registered components, you allow Simple Injector to build the entire object graph without having to revert to registering a code block, as we discussed in section 14.3.3. This is useful when working with Simple Injector because of the previously discussed diagnostic capabilities. The use of code blocks blinds a container, which might cause configuration mistakes to stay undetected for too long.
In the next section, you’ll see how to use the less ambiguous and more flexible approach where you allow any number of courses in a meal. To this end, you must learn how Simple Injector deals with lists and sequences.
In section 6.1.1, we discussed how Constructor Injection acts as a warning system for Single Responsibility Principle violations. The lesson then was that instead of viewing Constructor Over-injection as a weakness of the Constructor Injection pattern, you should rather rejoice that it makes a problematic design so obvious.
When it comes to DI Containers and ambiguity, we see a similar relationship. DI Containers generally don’t deal with ambiguity in a graceful manner. Although you can make a good DI Container like Simple Injector deal with it, it can seem awkward. This is often an indication that you could improve the design of your code.
Instead of feeling constrained by Simple Injector, you should embrace its conventions and let it guide you toward a better and more consistent design. In this section, we’ll look at an example that demonstrates how you can refactor away from ambiguity, as well as show how Simple Injector deals with sequences.
通过消除歧义重构更好的课程
Refactoring to a better course by removing ambiguity
在 14.4.1 节中,您看到了ThreeCourseMeal及其固有的歧义如何迫使您使注册复杂化。这应该会促使您重新考虑 API 设计。一个简单的概括转向IMeal采用任意数量的ICourse实例而不是恰好三个实例的实现,就像ThreeCourseMeal类的情况一样:
In section 14.4.1, you saw how the ThreeCourseMeal and its inherent ambiguity forced you to complicate your registration. This should prompt you to reconsider the API design. A simple generalization moves toward an implementation of IMeal that takes an arbitrary number of ICourse instances instead of exactly three, as was the case with the ThreeCourseMeal class:
public Meal(IEnumerable<ICourse> courses)
请注意,构造函数中不需要三个不同的实例,实例上ICourse的单个依赖IEnumerable<ICourse>项允许您向类提供任意数量的课程Meal——从零到……很多!这解决了含糊不清的问题,因为现在只有一个Dependency。此外,它还通过提供一个单一的通用类来改进 API 和实现,该类可以模拟不同类型的膳食:从只有一道菜的简单膳食到精心制作的 12 道菜晚餐。
Notice that, instead of requiring three distinct ICourse instances in the constructor, the single Dependency on an IEnumerable<ICourse> instance lets you provide any number of courses to the Meal class — from zero to ... a lot! This solves the issue with ambiguity, because there’s now only a single Dependency. In addition, it also improves the API and implementation by providing a single, general-purpose class that can model different types of meal: from a simple meal with a single course to an elaborate 12-course dinner.
In this section, we’ll look at how you can configure Simple Injector to wire up Meal instances with appropriate ICourseDependencies. When you’re done, you should have a good idea of the options available when you need to configure instances with sequences of Dependencies.
Simple Injector has a good understanding of sequences, so if you want to use all registered components of a given service, Auto-Wiring just works. As an example, given a set of configured ICourse instances, you can configure the IMeal service like this:
Notice that this is a completely standard mapping from an Abstraction to a concrete type. Simple Injector automatically understands the Meal constructor and determines that the correct course of action is to resolve all ICourse components. When you resolve IMeal, you get a Meal instance with the ICourse components. This still requires you to register the sequence of ICourse components, for instance, using Auto-Registration:
Simple Injector automatically handles sequences, and unless you specify otherwise, it does what you’d expect it to do: it resolves a sequence of Dependencies for all registrations of that Abstraction. Only when you need to explicitly pick only some components from a larger set do you need to do more. Let’s see how you can do that.
Simple Injector’s default strategy of injecting all components is often the correct policy, but as figure 14.5 shows, there may be cases where you want to pick only some registered components from the larger set of all registered components.
When you previously let Simple Injector Auto-Register and Auto-Wire all configured instances, it corresponded to the situation depicted on the right side of the figure. If you want to register a component as shown on the left side, you must explicitly define which components should be used. In order to achieve this, you can use the Collection.Create method, which allows creating a subset of a sequence. The following listing shows how to inject a subset of a sequence into a consumer.
Listing 14.10 Injecting a sequence subset into a consumer
IEnumerable<ICourse> coursesSubset1 =
container.Collection.Create<ICourse>( ① typeof(Rillettes), ① typeof(CordonBleu), ① typeof(MousseAuChocolat)); ①
IEnumerable<ICourse> coursesSubset2 =
container.Collection.Create<ICourse>( ② typeof(CeasarSalad), ② typeof(ChiliConCarne), ② typeof(MousseAuChocolat)); ② container.RegisterInstance<IMeal>( ③ new Meal(sourcesSubset1)); ③
The Collection.Create method lets you create a sequence of a given Abstraction. The sequence itself won’t be registered in the container — this can be done using Collection.Register. By calling Collection.Create multiple times for the same Abstraction, you can create multiple sequences that are all different subsets, as shown in listing 14.10.
What might be surprising about listing 14.10 is that the call to Collection.Create doesn’t create the courses at that point in time. Instead, the sequence is a stream. Only when you start iterating the sequence will it start to resolve instances. Because of this behavior, the sequence subset can be safely injected into the SingletonMeal without causing any harm. We’ll go into more detail about streams in section 14.4.5.
Simple Injector natively understands sequences. Unless you need to explicitly pick only some components from all services of a given type, Simple Injector automatically does the right thing.
Auto-Wiring works not only with single instances, but also for sequences; the container maps a sequence to all configured instances of the corresponding type. A perhaps less intuitive use of having multiple instances of the same Abstraction is the Decorator design pattern, which we’ll discuss next.
In section 9.1.1, we discussed how the Decorator design pattern is useful when implementing Cross-Cutting Concerns. By definition, Decorators introduce multiple types of the same Abstraction. At the very least, you have two implementations of an Abstraction: the Decorator itself and the decorated type. If you stack the Decorators, you can have even more. This is another example of having multiple registrations of the same service. Unlike the previous sections, these registrations aren’t conceptually equal, but rather Dependencies of each other.
Simple Injector has built-in support for registering Decorators using the RegisterDecorator method. And, in this section, we’ll discuss both registrations of non-generic and generic Abstractions. Let’s start with the former.
Using the RegisterDecorator method, you can elegantly register a Decorator. The following example shows how to use this method to apply Breading to a VealCutlet:
var c = new Container();
c.Register<IIngredient, VealCutlet>(); ① c.RegisterDecorator<IIngredient, Breading>(); ②
As you learned in chapter 9, you get veal cordon bleu when you slit open a pocket in the veal cutlet and add ham, cheese, and garlic into the pocket before breading the cutlet. The following example shows how to add a HamCheeseGarlic Decorator in between VealCutlet and the Breading Decorator:
var c = new Container();
c.Register<IIngredient, VealCutlet>();
c.RegisterDecorator<IIngredient, HamCheeseGarlic>(); ①
c.RegisterDecorator<IIngredient, Breading>();
By placing this new registration before the Breading registration, the HamCheeseGarlic Decorator will be wrapped first. This results in an object graph equal to the following Pure DI version:
new Breading( ① new HamCheeseGarlic( ① new VealCutlet())); ①
During the course of chapter 10, we defined multiple generic Decorators that could be applied to any ICommandService<TCommand> implementation. In the remainder of this chapter, we’ll set our ingredients and courses aside, and take a look at how to register these generic Decorators using Simple Injector. The following listing demonstrates how to register all ICommandService<TCommand> implementations with the three Decorators presented in section 10.3.
container.Register( ① typeof(ICommandService<>), assembly); ① container.RegisterDecorator( ② typeof(ICommandService<>), ② typeof(AuditingCommandServiceDecorator<>)); ② ② container.RegisterDecorator( ② typeof(ICommandService<>), ② typeof(TransactionCommandServiceDecorator<>)); ② ② container.RegisterDecorator( ② typeof(ICommandService<>), ② typeof(SecureCommandServiceDecorator<>)); ②
As in listing 14.3, you use a Register overload to register arbitrary ICommandService<TCommand> implementations by scanning assemblies. To register generic Decorators, you use the RegisterDecorator method that accepts two Type instances. The result of the configuration of listing 14.11 is figure 14.6, which we discussed previously in section 10.3.4.
When it comes to Simple Injector’s support for Decorators, this is only the tip of the iceberg. Several RegisterDecorator overloads allow Decorators to be made conditionally, like the previously discussed RegisterConditional overload of listing 14.9. A discussion of this and other features, however, is out of the scope of this book.9
Simple Injector lets you work with multiple Decorator instances in several different ways. You can register components as alternatives to each other, as peers resolved as sequences, or as hierarchical Decorators. In many cases, Simple Injector figures out what to do. You can always explicitly define how services are composed if you need more-explicit control.
In this section, we focused on Simple Injector’s methods that were explicitly designed for configuring Decorators. Although consumers that rely on sequences of Dependencies can be the most intuitive use of multiple instances of the same Abstraction, Decorators are another good example. But there’s a third and perhaps a bit surprising case where multiple instances come into play, which is the Composite design pattern.
During the course of this book, we discussed the Composite design pattern on several occasions. In section 6.1.2, for instance, you created a CompositeNotificationService (listing 6.4) that both implemented INotificationService and wrapped a sequence of INotificationService implementations.
Let’s take a look at how you can register Composites, such as the CompositeNotificationService from chapter 6 in Simple Injector. The following listing shows this class again.
Listing 14.12 The CompositeNotificationService Composite from chapter 6
public class CompositeNotificationService : INotificationService
{
private readonly IEnumerable<INotificationService> services;
public CompositeNotificationService(
IEnumerable<INotificationService> services)
{
this.services = services;
}
public void OrderApproved(Order order)
{
foreach (INotificationService service in this.services)
{
service.OrderApproved(order);
}
}
}
由于 Simple Injector API 将序列注册与非序列注册分开,因此 Composites 的注册再简单不过了。您可以将 Composite 注册为单个注册,同时将其依赖项注册为序列:
Because the Simple Injector API separates the registration of sequences from non-sequence registrations, the registration of Composites couldn’t be any easier. You can register the Composite as a single registration, while registering its Dependencies as a sequence:
In the previous example, three INotificationService implementations are registered as a sequence using Collection.Register. The CompositeNotificationService, on the other hand, is registered as single, non-sequence registration. All types are Auto-Wired by Simple Injector. Using the previous registration, when an INotificationService is resolved, it results in an object graph similar to the following Pure DI representation:
return new CompositeNotificationService(new INotificationService[]
{
new OrderApprovedReceiptSender(),
new AccountingNotifier(),
new OrderFulfillment()
});
Because the number of notification services will likely grow over time, you can reduce the burden on your Composition Root by applying Auto-Registration using the Collection.Register overload that accepts an Assembly. This lets you turn the previous list of types into a simple one-liner:
You may recall from chapter 13 that a similar construct in Autofac didn’t work, because Autofac’s Auto-Registration would register the Composite as well as part of the sequence. This, however, isn’t the case with Simple Injector. It’s Collection.Register method automatically filters out any Composite types and prevents them from being registered as part of the sequence.
Composite classes, however, aren’t the only classes that will automatically be removed from the list by Simple Injector. Simple Injector also detects Decorators in the same way. This behavior makes working with Decorators and Composites in Simple Injector a breeze. The same holds true for working with generic Composites.
In section 14.4.2, you saw how Simple Injector’s RegisterDecorator method made registering generic Decorators look like child’s play. In this section, we’ll take a look at how you can register Composites for generic Abstractions.
In section 6.1.3, you specified the CompositeEventHandler<TEvent> class (listing 6.12) as a Composite implementation over a sequence of IEventHandler<TEvent> implementations. Let’s see if you can register the Composite with its wrapped event handler implementations. We’ll start with the Auto-Registration of the event handlers:
与清单 14.3ICommandService<T>中的实现注册相比,您现在使用instead of 。那是因为对于特定类型的事件可能有多个处理程序。这意味着您必须明确声明您知道单个事件类型会有更多实现。如果您不小心调用了而不是,Simple Injector 会抛出类似于以下的异常:Collection.RegisterRegisterRegisterCollection.Register
In contrast to the registration of ICommandService<T> implementations in listing 14.3, you now use Collection.Register instead of Register. That’s because there’ll potentially be multiple handlers for a particular type of event. This means you have to explicitly state that you know there’ll be more implementations for the single event type. Were you to have accidentally called Register instead of Collection.Register, Simple Injector would have thrown an exception similar to the following:
In the supplied list of types or assemblies, there are 3 types that represent the same closed-generic type IEventHandler<OrderApproved>. Did you mean to register the types as a collection using the Collection.Register method instead? Conflicting types: OrderApprovedReceiptSender, AccountingNotifier, and OrderFulfillment.
A nice thing about this message is that it already indicates you most likely should be using Collection.Register instead of Register. But it’s also possible that you accidentally added an invalid type that was picked up. As we explained before, when it comes to ambiguity, Simple Injector forces you to be explicit, which is helpful in detecting errors.
What remains is the registration for CompositeEventHandler<TEvent>. Because CompositeEventHandler<TEvent> is a generic type, you’ll have to use the Register overload that accepts Type arguments:
container.Register( ① typeof(IEventHandler<>), ① typeof(CompositeEventHandler<>)); ①
Using this registration, when a particular closed IEventHandler<TEvent>Abstraction is requested (for example, IEventHandler<OrderApproved>), Simple Injector determines the exact CompositeEventHandler<TEvent> type to create. In this case, this is rather straightforward, because requesting an IEventHandler<OrderApproved> results in a CompositeEventHandler<OrderApproved> getting resolved. In other cases, determining the exact closed type can be a rather complex process, but Simple Injector handles this well.
Working with sequences is rather straightforward in Simple Injector. When it comes to resolving and injecting sequences, however, Simple Injector behaves differently compared to other DI Containers in a captivating way. As we alluded earlier, Simple Injector handles sequences as streams.
14.4.5 序列是流
14.4.5 Sequences are streams
在第 14.1 节中,您注册了一系列成分,如下所示:
In section 14.1, you registered a sequence of ingredients as follows:
As shown previously, you can ask the container to resolve all IIngredient components using either the GetAllInstances or GetInstance methods. Here’s the example using GetInstance again:
您可能希望调用 来GetInstance<IEnumerable<IIngredient>>()创建两个类的实例,但这与事实相差甚远。解析或注入 时IEnumerable<T>,Simple Injector 不会立即用所有成分预填充序列。相反,IEnumerable<T>表现得像一个流。10 这意味着返回IEnumerable<IIngredient>的是一个能够在IIngredient迭代时生成新实例的对象。这类似于使用 a 从磁盘流式传输数据System.IO.FileStream或使用 a 从数据库流式传输数据,其中数据以小块的形式到达,而不是一次性预取所有数据。System.Data.SqlClient.SqlDataReader
You might expect the call to GetInstance<IEnumerable<IIngredient>>() to create an instance of both classes, but this couldn’t be further from the truth. When resolving or injecting an IEnumerable<T>, Simple Injector doesn’t prepopulate the sequence with all ingredients right away. Instead, IEnumerable<T> behaves like a stream.10 What this means is that the returned IEnumerable<IIngredient> is an object that’s able to produce new IIngredient instances when it’s iterated. This is similar to streaming data from disk using a System.IO.FileStream or a database using a System.Data.SqlClient.SqlDataReader, where data arrives in small chunks rather than prefetching all the data in one go.
以下示例显示了多次迭代流如何生成新实例:
The following example shows how iterating a stream multiple times can produce new instances:
IEnumerable<IIngredient> stream =
container.GetAllInstance<IIngredient>();
IIngredient ingredient1 = stream.First(); ① IIngredient ingredient2 = stream.First(); ② object.ReferenceEquals(ingredient1, ingredient2); ③
When a stream is iterated, it calls back into the container to resolve elements of the sequence based on their appropriate Lifestyle. This means that if the type is registered as Transient, new instances are always produced, as the previous example showed. When the type is Singleton, however, the same instance is returned every time:
var c = new Container();
c.Collection.Append<IIngredient, SauceBéarnaise>(); ① c.Collection.Append<IIngredient, Steak>( ① Lifestyle.Singleton); ①
var s = c.GetInstance<IEnumerable<IIngredient>>();
object.ReferenceEquals(s.First(), s.First()); ② object.ReferenceEquals(s.Last(), s.Last()); ③
Although streaming isn’t a common trait under DI Containers, it has a few interesting advantages. First, when injecting a stream into a consumer, the injection of the stream itself is practically free, because no instance is created at that point in time.11 This is useful when the list of elements is big, and not all elements are needed during the lifetime of the consumer. Take the following Composite ILogger implementation, for instance. It’s a variation of the Composite of listing 8.22 but, in this case, the Composite stops logging directly after one of the wrapped loggers succeeds.
在本例中,您将 the 注册CompositeLogger为Singleton,因为它是无状态的,并且它唯一的依赖项theIEnumerable<ILogger>本身就是一个Singleton。CompositeLogger和ILogger序列作为单例的效果是注入CompositeLogger实际上是免费的。即使当消费者调用其Dependency的Log方法时,这通常只会导致创建ILogger序列的第一个实现——而不是所有的实现。
In this case, you registered the CompositeLogger as Singleton because it’s stateless, and its only Dependency, the IEnumerable<ILogger>, is itself a Singleton. The effect of the CompositeLogger and ILogger sequences as Singletons is that the injecting of CompositeLogger is practically free. Even when a consumer calls its Dependency’s Log method, this typically only results in the creation of the first ILogger implementation of the sequence — not all of them.
A second advantage of sequences being streams is that, as long as you only store the reference to IEnumerable<ILogger>, as listing 14.13 showed, the sequence’s elements can never accidentally become Captive Dependencies. The previous example already showed this. The SingletonCompositeLogger could safely depend on IEnumerable<ILogger>, because it also is a Singleton, even though its produced services might not be.
In this section, you’ve seen how to deal with multiple components such as sequences, Decorators, and Composites. This ends our discussion of Simple Injector. In the next chapter, we’ll turn our attention to Microsoft.Extensions.DependencyInjection.
概括
Summary
Simple Injector 是一个现代的DI 容器,提供了相当全面的功能集,但它的 API 与大多数DI 容器有很大不同。以下是它的一些特征属性:
范围是环境的。
序列是使用Collection.Register而不是附加相同抽象的新注册来注册的。
序列表现为流。
可以诊断容器以发现常见的配置陷阱。
Simple Injector is a modern DI Container that offers a fairly comprehensive feature set, but its API is quite different from most DI Containers. The following are a few of its characteristic attributes:
Scopes are ambient.
Sequences are registered using Collection.Register instead of appending new registrations of the same Abstraction.
Sequences behave as streams.
The container can be diagnosed to find common configuration pitfalls.
Simple Injector 的一个重要整体主题是严格性。它不会尝试猜测您的意思,而是会尝试通过其 API 和诊断工具来防止和检测配置错误。
An important overall theme for Simple Injector is one of strictness. It doesn’t attempt to guess what you mean and tries to prevent and detect configuration errors through its API and diagnostic facility.
Simple Injector enforces a strict separation of registration and resolution. Although you use the same Container instance for both register and resolve, the Container is locked after first use.
Because of Simple Injector’s ambient scopes, resolving from the root container directly is good practice and encouraged: it doesn’t lead to memory leaks or concurrency bugs.
Simple Injector supports the standard Lifestyles: Transient, Singleton, and Scoped.
Simple Injector 对序列、装饰器、合成器和泛型的注册有很好的支持。
Simple Injector has excellent support for registration of sequences, Decorators, Composites, and generics.
15
Microsoft.Extensions.DependencyInjection DI 容器
15
The Microsoft.Extensions.DependencyInjection DI Container
在这一章当中
In this chapter
使用 Microsoft.Extensions.DependencyInjection 的注册 API
Working with Microsoft.Extensions.DependencyInjection’s registration API
管理组件生命周期
Managing component lifetime
配置困难的 API
Configuring difficult APIs
配置序列、装饰器和组合
Configuring sequences, Decorators, and Composites
随着 ASP.NET Core 的推出,微软引入了自己的DI 容器Microsoft.Extensions.DependencyInjection,作为 Core 框架的一部分。在本章中,我们将该名称简称为MS.DI。
With the introduction of ASP.NET Core, Microsoft introduced its own DI Container, Microsoft.Extensions.DependencyInjection, as part of the Core framework. In this chapter, we shorten that name to MS.DI.
Microsoft 构建 MS.DI 是为了简化使用 ASP.NET Core 的框架和第三方组件开发人员的依赖关系管理。Microsoft 的意图是定义一个DI 容器,它具有所有其他DI 容器都可以遵循的最小的、最低的公分母功能集。
Microsoft built MS.DI to simplify Dependency management for framework and third-party component developers working with ASP.NET Core. Microsoft’s intention was to define a DI Container with a minimal, lowest common denominator feature set that all other DI Containers could conform to.
In this chapter, we’ll give MS.DI the same treatment that we gave Autofac and Simple Injector. You’ll see to which degree MS.DI can be used to apply the principles and patterns laid forth in parts 1–3. Even though MS.DI is integrated in ASP.NET Core, it can also be used separately, which is why, in this chapter, we treat it as such.
During the course of this chapter, however, you’ll find that MS.DI is so limited in functionality that we deem it unsuited for development of any reasonably sized application that practices loose coupling and follows the principles and patterns described in this book. If MS.DI isn’t suited, then why use an entire chapter covering it in this book? The most important reason is that MS.DI looks at a first glance so much like the other DI Containers that you need to spend some time with it to understand the differences between it and mature DI Containers. Because it’s part of .NET Core, it may be tempting to use this built-in container if you don’t understand its limitations. The purpose of this chapter is to reveal these limitations so you can make an informed decision.
This chapter is divided into four sections. You can read each section independently, though the first section is a prerequisite for the other sections, and the fourth section relies on some methods and classes introduced in the third section. You can read the chapter in isolation from the rest of part 4, specifically to learn about MS.DI, or you can read it together with the other chapters to compare DI Containers. The focus of this chapter is to show how MS.DI relates to and implements the patterns and principles described in parts 1–3.
In this section, you’ll learn where to get MS.DI, what you get, and how you start using it. We’ll also look at common configuration options. Table 15.1 provides fundamental information that you’re likely to need to get started.
At a high level, using MS.DI isn’t that different from Autofac (discussed in chapter 13). Its usage is a two-step process, as figure 15.1 illustrates. Compared to Simple Injector, however, with MS.DI this two-step process is explicit: first, you configure a ServiceCollection, and when you’re done with that, you use it to build a ServiceProvider that can be used to resolve components.
Figure 15.1 The pattern for using Microsoft.Extensions.DependencyInjection is to first configure it and then resolve components.
完成本节后,您应该对 MS.DI 的整体使用模式有一个良好的感觉,并且您应该能够在行为良好的场景中开始使用它——所有组件都遵循正确的 DI 模式,例如构造函数注入。让我们从最简单的场景开始,看看如何使用 MS.DI 容器解析对象。
When you’re done with this section, you should have a good feeling for the overall usage pattern of MS.DI, and you should be able to start using it in well-behaved scenarios — where all components follow proper DI patterns, such as Constructor Injection. Let’s start with the simplest scenario and see how you can resolve objects using an MS.DI container.
The core service of any DI Container is to compose object graphs. In this section, we’ll look at the API that enables you to compose object graphs with MS.DI. MS.DI requires you to register all relevant components before you can resolve them. The following listing shows one of the simplest possible uses of MS.DI.
As was already implied by figure 15.1, you need a ServiceCollection instance to configure components. MS.DI’s ServiceCollection is the equivalent of Autofac’s ContainerBuilder.
Here, you register the concrete SauceBéarnaise class with services, so that when you ask it to build a container, the resulting container is configured with the SauceBéarnaise class. This again enables you to resolve the SauceBéarnaise class from the container. If you don’t register the SauceBéarnaise component, the attempt to resolve it throws a InvalidOperationException with the following message:
As listing 15.1 shows, with MS.DI, you never resolve from the root container itself but from an IServiceScope. Section 15.2.1 goes into more detail about what an IServiceScope is.
作为一项安全措施,始终ServiceProvider使用带参数的BuildServiceProvider重载来构建validateScopes设置为true,如代码清单 15.1所示。这可以防止从根容器中意外解析Scoped实例。随着 ASP.NET Core 2.0 的引入,当应用程序在开发环境中运行时,由框架validateScopes自动设置为true,但最好在开发环境之外也启用验证。这意味着您必须手动呼叫。BuildServiceProvider(true)
As a safety measure, always build the ServiceProvider using the BuildServiceProvider overload with the validateScopes argument set to true, as shown in listing 15.1. This prevents the accidental resolution of Scoped instances from the root container. With the introduction of ASP.NET Core 2.0, validateScopes is automatically set to true by the framework when the application is running in the development environment, but it’s best to enable validation even outside the development environment as well. This means you’ll have to call BuildServiceProvider(true) manually.
Not only can MS.DI resolve concrete types with parameterless constructors, it can also Auto-Wire a type with other Dependencies. All these Dependencies need to be registered. For the most part, you want to program to interfaces, because this introduces loose coupling. To support this, MS.DI lets you map Abstractions to concrete types.
Whereas our application’s root types will typically be resolved by their concrete types as listing 15.1 showed, loose coupling requires you to map Abstractions to concrete types. Creating instances based on such maps is the core service offered by any DI Container, but you must still define the map. In this example, you map the IIngredient interface to the concrete SauceBéarnaise class, which allows you to successfully resolve IIngredient:
var services = new ServiceCollection();
services.AddTransient<IIngredient, SauceBéarnaise>(); ①
var container = services.BuildServiceProvider(true);
IServiceScope scope = container.CreateScope();
IIngredient sauce = scope.ServiceProvider
.GetRequiredService<IIngredient>(); ②
Here, the AddTransient method allows a concrete type to be mapped to a particular Abstraction using the Transient Lifestyle. Because of the previous AddTransient call, SauceBéarnaise can now be resolved as IIngredient.
在许多情况下,通用 API 就是您所需要的。不过,在某些情况下,您需要一种更弱类型的方式来解析服务。这也是可能的。
In many cases, the generic API is all you need. Still, there are situations where you’ll need a more weakly typed way to resolve services. This is also possible.
解决弱类型服务
Resolving weakly typed services
有时您不能使用通用 API,因为您在设计时不知道合适的类型。您只有一个Type实例,但您仍然希望获得该类型的实例。您在 7.3 节中看到了一个示例,我们在其中讨论了 ASP.NET Core MVC 的IControllerActivator类。相关的方法是这个:
Sometimes you can’t use a generic API because you don’t know the appropriate type at design time. All you have is a Type instance, but you’d still like to get an instance of that type. You saw an example of that in section 7.3, where we discussed ASP.NET Core MVC’s IControllerActivator class. The relevant method is this one:
As shown previously in listing 7.8, the ControllerContext captures the controller’s Type, which you can extract using the ControllerTypeInfo property of the ActionDescriptor property:
Type controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType();
Because you only have a Type instance, you can’t use generics, but must resort to a weakly typed API. MS.DI offers a weakly typed overload of the GetRequiredService method that lets you implement the Create method:
Type controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType();
return scope.ServiceProvider.GetRequiredService(controllerType);
的弱类型重载允许您传递变量GetRequiredServicecontrollerType直接到 MS.DI。通常,这意味着您必须将返回值转换为某种抽象,因为弱类型GetRequiredService方法回报object。但是,在 的情况下IControllerActivator,这不是必需的,因为 ASP.NET Core MVC 不需要控制器来实现任何接口或基类。
The weakly typed overload of GetRequiredService lets you pass the controllerType variable directly to MS.DI. Typically, this means you have to cast the returned value to some Abstraction, because the weakly typed GetRequiredService method returns object. In the case of IControllerActivator, however, this isn’t required, because ASP.NET Core MVC doesn’t require controllers to implement any interface or base class.
No matter which overload of GetRequiredService you use, MS.DI guarantees that it’ll return an instance of the requested type or throw an exception if there are Dependencies that can’t be satisfied. When all required Dependencies have been properly configured, MS.DI can Auto-Wire the requested type.
To be able to resolve the requested type, all loosely coupled Dependencies must have been previously configured. Let’s investigate the ways that you can configure MS.DI.
As we discussed in section 12.2, you can configure a DI Container in several conceptually different ways. Figure 12.5 reviewed the options: configuration files, Configuration as Code, and Auto-Registration. Figure 15.2 shows these options again.
Although there’s no Auto-Registration API, to some extent, you can implement assembly scanning with the help of .NET’s LINQ and reflection APIs. Before we discuss this, we’ll start with a discussion of MS.DI’s Configuration as Code API.
配置ServiceCollection使用配置作为代码
Configuring the ServiceCollection using Configuration as Code
All configuration in MS.DI uses the API exposed by the ServiceCollection class, although most of the methods are extension methods. One of the most commonly used methods is the AddTransient method that you’ve already seen:
Registering SauceBéarnaise as IIngredient hides the concrete class so that you can no longer resolve SauceBéarnaise with this registration. But you can fix this by replacing the registration with the following:
services.AddTransient<SauceBéarnaise>();
services.AddTransient<IIngredient>(
c => c.GetRequiredService<SauceBéarnaise>()); ①
Instead of making the registration for IIngredient using the Auto-Wiring overload of AddTransient, you register a code block that, when called, forwards the call to the registration of the concrete SauceBéarnaise.
In real applications, you always have more than one Abstraction to map, so you must configure multiple mappings. This is done with multiple calls to one of the Add... methods:
This maps IIngredient to SauceBéarnaise, and ICourse to Course. There’s no overlap of types, so it should be pretty evident what’s going on. But you can also register the same Abstraction several times:
Here, you register IIngredient twice. If you resolve IIngredient, you get an instance of Steak. The last registration wins, but previous registrations aren’t forgotten. MS.DI can handle multiple configurations for the same Abstraction, but we’ll get back to this topic in section 15.4.
Although there are more-advanced options available for configuring MS.DI, you can configure an entire application with the methods shown here. But to save yourself from too much explicit maintenance of container configuration, you could instead consider a more convention-based approach using Auto-Registration.
ServiceCollection使用自动注册进行配置
Configuring ServiceCollection using Auto-Registration
In many cases, registrations will be similar. Such registrations are tedious to maintain, and explicitly registering each and every component might not be the most productive approach, as we discussed in section 12.3.3.
Consider a library that contains many IIngredient implementations. You can configure each class individually, but it’ll result in an ever-changing list of Type instances supplied to the Add... methods. What’s worse is that every time you add a new IIngredient implementation, you must also explicitly register it with the container if you want it to be available. It would be more productive to state that all implementations of IIngredient found in a given assembly should be registered.
As stated previously, MS.DI contains no Auto-Registration API. This means you have to do it yourself. This is possible to some degree, and in this section, we’ll show how with a simple example but delay more detailed discussions of the possibilities and limitations until section 15.4. Let’s take a look how you can register a sequence of IIngredient registrations:
Assembly ingredientsAssembly = typeof(Steak).Assembly;
var ingredientTypes =
from type in ingredientsAssembly.GetTypes() ① where !type.IsAbstract ① where typeof(IIngredient).IsAssignableFrom(type) ① select type; ①
foreach (var type in ingredientTypes)
{
services.AddTransient(typeof(IIngredient), type); ②
}
The previous example unconditionally configures all implementations of the IIngredient interface, but you can provide filters that enable you to select only a subset. Here’s a convention-based scan where you add only classes whose name starts with Sauce:
Assembly ingredientsAssembly = typeof(Steak).Assembly;
var ingredientTypes =
from type in ingredientsAssembly.GetTypes()
where !type.IsAbstract
where typeof(IIngredient).IsAssignableFrom(type)
where type.Name.StartsWith("Sauce") ①
select type;
foreach (var type in ingredientTypes)
{
services.AddTransient(typeof(IIngredient), type);
}
Apart from selecting the correct types from an assembly, another part of Auto-Registration is defining the correct mapping. In the previous examples, you used the AddTransient method with a specific interface to register all selected types against that interface.
But sometimes you’ll want to use different conventions. Let’s say that instead of interfaces, you use abstract base classes, and you want to register all types in an assembly where the name ends with Policy by their base type:
Assembly policiesAssembly = typeof(DiscountPolicy).Assembly;
var policyTypes =
from type in policiesAssembly.GetTypes() ① where type.Name.EndsWith("Policy") ②
select type;
foreach (var type in policyTypes)
{
services.AddTransient(type.BaseType, type); ③
}
Even though MS.DI contains no convention-based API, by making use of existing .NET framework APIs, convention-based registrations are possible. This becomes a different ball game when it comes to generics, as we’ll discuss next.
During the course of chapter 10, you refactored the big, obnoxious IProductService interface to the ICommandService<TCommand> interface of listing 10.12. Here’s that Abstraction again:
public interface ICommandService<TCommand>
{
void Execute(TCommand command);
}
As discussed in chapter 10, every command Parameter Object represents a use case, and there’ll be a single implementation per use case. The AdjustInventoryService of listing 10.8 was given as an example. It implemented the “adjust inventory” use case. The following listing shows this class again.
Any reasonably complex system will easily implement hundreds of use cases, and this is an ideal candidate for using Auto-Registration. But because of the lack of Auto-Registration support by MS.DI, you’ll have to write a fair amount of code to get this running. The next listing provides an example of this.
Listing 15.3Auto-Registration of ICommandService<TCommand> implementations
Assembly assembly = typeof(AdjustInventoryService).Assembly;
var mappings =
from type in assembly.GetTypes()
where !type.IsAbstract ① where !type.IsGenericType ② from i in type.GetInterfaces() ③ where i.IsGenericType ③ where i.GetGenericTypeDefinition() ③ == typeof(ICommandService<>) ③ select new { service = i, type }; ③
foreach (var mapping in mappings)
{
services.AddTransient( ④ mapping.service, ④ mapping.type); ④
}
与前面的清单一样,您可以充分利用 .NET 的 LINQ 和反射 API 来允许从提供的程序集中选择类。使用提供的开放式通用接口,您可以遍历程序集类型列表,并注册实现封闭式通用版本的所有类型ICommandService<TCommand>。例如,这意味着它AdjustInventoryService已注册,因为它实现ICommandService<AdjustInventory>了 ,它是 . 的封闭通用版本ICommandService<TCommand>。
As in the previous listings, you make full use of .NET’s LINQ and Reflection APIs to allow selecting classes from the supplied assembly. Using the supplied open-generic interface, you iterate through the list of assembly types, and register all types that implement a closed-generic version of ICommandService<TCommand>. What this means, for instance, is that AdjustInventoryService is registered because it implements ICommandService<AdjustInventory>, which is a closed-generic version of ICommandService<TCommand>.
本节介绍了 MS.DI DI 容器并演示了这些基本机制:如何配置ServiceCollection,以及随后如何使用构造ServiceProvider的来解析服务。只需调用一次GetRequiredService方法即可解析服务,因此复杂性涉及配置容器。API 主要支持Configuration as Code,尽管在某种程度上可以在其之上构建自动注册。但是,正如您稍后将看到的那样,不支持自动注册将导致代码非常复杂且难以维护。到目前为止,我们只了解了最基本的 API,但我们还没有涉及另一个领域——如何管理组件生命周期。
This section introduced the MS.DI DI Container and demonstrated these fundamental mechanics: how to configure a ServiceCollection, and, subsequently, how to use the constructed ServiceProvider to resolve services. Resolving services is done with a single call to the GetRequiredService method, so the complexity involves configuring the container. The API primarily supports Configuration as Code, although to some extend Auto-Registration can be built on top of it. As you’ll see later, however, the lack of support for Auto-Registration will lead to quite complex and hard-to-maintain code. Until now, we’ve only looked at the most basic API, but there’s another area we have yet to cover — how to manage component lifetime.
15.2 管理生命周期
15.2 Managing lifetime
在第 8 章中,我们讨论了生命周期管理,包括最常见的概念生命周期样式,例如Transient、Singleton和Scoped。MS.DI 支持这三种Lifestyles,并允许您配置所有服务的生命周期。表 15.2中显示的生活方式作为 API 的一部分提供。
In chapter 8, we discussed Lifetime Management, including the most common conceptual lifetime styles such as Transient, Singleton, and Scoped. MS.DI supports these three Lifestyles and lets you configure the lifetime of all services. The Lifestyles shown in table 15.2 are available as part of the API.
MS.DI’s implementation of Transient and Singleton are equivalent to the general Lifestyles described in chapter 8, so we won’t spend much time on them in this chapter. Instead, in this section, you’ll see how you can define Lifestyles for components in code. By the end of this section, you should be able to use MS.DI’s Lifestyles in your own application. Let’s start by reviewing how to configure instance scopes for components.
This configures the concrete SauceBéarnaise class as a Singleton so that the same instance is returned each time SauceBéarnaise is requested. If you want to map an Abstraction to a concrete class with a specific Lifestyle, you can use the AddSingleton overload with two generic arguments:
Compared to other DI Containers, there aren’t many options in MS.DI when it comes to configuring Lifestyles for components. It’s done in a rather declarative fashion. Although configuration is typically easy, you mustn’t forget that some Lifestyles involve long-lived objects that use resources as long as they’re around.
As discussed in section 8.2.2, it’s important to release objects when you’re done with them. Similar to Autofac and Simple Injector, MS.DI has no explicit Release method, but instead uses a concept called scopes. A scope can be regarded as a request-specific cache. As figure 15.3 illustrates, it defines a boundary where components can be reused.
An IServiceScope defines a cache that you can use for a particular duration or purpose; the most obvious example is a web request. When a Scoped component is requested from an IServiceScope, you always receive the same instance. The difference from true Singletons is that if you query a second scope, you’ll get another instance.
One of the important features of scopes is that they let you properly release components when the scope completes. You create a new scope with the CreateScope method of a particular IServiceProvider implementation, and release all appropriate components by invoking its Dispose method:
using (IServiceScope scope = container.CreateScope()) ①
{
IMeal meal = scope.ServiceProvider
.GetRequiredService<IMeal>(); ② meal.Consume(); ③ } ④
A new scope is created from the container by invoking the CreateScope method. The return value implements IDisposable, so you can wrap it in a using block. Because IServiceScope contains a ServiceProvider property that implements the same interface that the container itself implements, you can use the scope to resolve components in exactly the same way as with the container itself.
When you’re done with the scope, you can dispose of it. With a using block, this happens automatically when you exit that block, but you can also choose to explicitly dispose of it by invoking the Dispose method. When you dispose of the scope, you also release all the components that were created by the scope; here, it means that you release the meal object graph.
Note that Dependencies of a component are always resolved at or below the component’s scope. For example, if you need a Transient Dependency injected into a Singleton, that Transient Dependency will come from the root container, even if you’re resolving the Singleton from a nested scope. This tracks the Transient within the root container and prevents it from being disposed of when the scope gets disposed of. The Singleton consumer would otherwise break, because it’s kept alive in the root container while depending on a component that was disposed of.
Earlier in this section, you saw how to configure components as Singletons or Transients. Configuring a component to have a Scoped Lifestyle is done in a similar way:
Similar to the AddTransient and AddSingleton methods, you can use the AddScoped method to state that the component’s lifetime should follow the scope that created the instance.
Due to their nature, Singletons are never released for the lifetime of the container itself. Still, you can release even those components if you don’t need the container any longer. This is done by disposing of the container itself:
In practice, this isn’t nearly as important as disposing of a scope, because the lifetime of a container tends to correlate closely with the lifetime of the application it supports. You normally keep the container around as long as the application runs, so you’d only dispose of it when the application shuts down. In this case, memory would be reclaimed by the operating system.
This completes our tour of Lifetime Management with MS.DI. Components can be configured with mixed Lifestyles, and this is true even when you register multiple implementations of the same Abstraction. Until now, you’ve allowed the container to wire Dependencies by implicitly assuming that all components use Constructor Injection. But this isn’t always the case. In the next section, we’ll review how to deal with classes that must be instantiated in special ways.
15.3 注册困难的 API
15.3 Registering difficult APIs
到目前为止,我们已经考虑了如何配置使用构造函数注入的组件。构造函数注入的众多好处之一是DI 容器(例如 MS.DI)可以轻松理解如何在依赖图中组合和创建所有类。当 API 表现不佳时,这一点就不太清楚了。
Until now, we’ve considered how you can configure components that use Constructor Injection. One of the many benefits of Constructor Injection is that DI Containers such as MS.DI can easily understand how to compose and create all classes in a Dependency graph. This becomes less clear when APIs are less well behaved.
In this section, you’ll see how to deal with primitive constructor arguments and static factories. These all require your special attention. Let’s start by looking at classes that take primitive types, such as strings or integers, as constructor arguments.
As long as you inject Abstractions into consumers, all is well. But it becomes more difficult when a constructor depends on a primitive type, such as a string, a number, or an enum. This is particularly the case for data access implementations that take a connection string as constructor parameter, but it’s a more general issue that applies to all strings and numbers.
从概念上讲,将字符串或数字注册为容器中的组件并不总是有意义的。使用通用类型约束,MS.DI 甚至阻止从其通用 API 注册值类型,如数字和枚举。另一方面,对于非通用 API,这仍然是可能的。以这个构造函数为例:
Conceptually, it doesn’t always make sense to register a string or number as a component in a container. Using generic type constraints, MS.DI even blocks the registration of value types like numbers and enums from its generic API. With the non-generic API, on the other hand, this is still possible. Consider as an example this constructor:
When you subsequently resolve ChiliConCarne, it’ll have a Medium Spiciness, as will all other components with a Dependency on Spiciness. If you’d rather control the relationship between ChiliConCarne and Spiciness on a finer level, you can use a code block, which is something we get back to in a moment in section 15.3.3.
此处描述的选项使用自动装配为组件提供具体值。然而,更方便的解决方案是将原始依赖项提取到参数对象中。
The option described here uses Auto-Wiring to provide a concrete value to a component. A more convenient solution, however, is to extract the primitive Dependencies into Parameter Objects.
15.3.2 提取对参数对象的原始依赖
15.3.2 Extracting primitive Dependencies to Parameter Objects
In section 10.3.3, we discussed how the introduction of Parameter Objects allowed mitigating the Open/Closed Principle violation that IProductService caused. Parameter Objects, however, are also a great tool to mitigate ambiguity. For example, the Spiciness of a course could be described in more general terms as a flavoring. Flavoring might include other properties, such as saltiness, so you can wrap Spiciness and the saltiness in a Flavoring class:
public class Flavoring
{
public readonly Spiciness Spiciness;
public readonly bool ExtraSalty;
public Flavoring(Spiciness spiciness, bool extraSalty)
{
this.Spiciness = spiciness;
this.ExtraSalty = extraSalty;
}
}
As we mentioned in section 10.3.3, it’s perfectly fine for Parameter Objects to have one parameter. The goal is to remove ambiguity, and not just on the technical level. Such a Parameter Object’s name might do a better job describing what your code does on a functional level, as the Flavoring class so elegantly does. With the introduction of the Flavoring Parameter Object, it now becomes possible to Auto-Wire any ICourse implementation that requires some flavoring without introducing ambiguity:
var flavoring = new Flavoring(Spiciness.Medium, extraSalty: true);
services.AddSingleton<Flavoring>(flavoring);
container.AddTransient<ICourse, ChiliConCarne>();
This code creates a single instance of the Flavoring class. Flavoring becomes a configuration object for courses. Because there’ll only be one Flavoring instance, you can register it in MS.DI using the AddSingleton<T> overload that accepts a precreated instance.
Extracting primitive Dependencies into Parameter Objects should be your preference over the previously discussed option, because Parameter Objects remove ambiguity, at both the functional and technical levels. It does, however, require a change to a component’s constructor, which might not always be feasible. In this case, registering a delegate is your second-best pick.
15.3.3 使用代码块注册对象
15.3.3 Registering objects with code blocks
使用原始值创建组件的另一种选择是使用其中一种Add...方法,它允许您提供创建组件的委托:
Another option for creating a component with a primitive value is to use one of the Add... methods, which let you supply a delegate that creates the component:
services.AddTransient<ICourse>(c => new ChiliConCarne(Spiciness.Hot));
你已经看到了这个AddTransient方法之前我们在 15.1.2 节中讨论 torn Lifestyles时会超载。每次解析服务时都会ChiliConCarne调用构造函数。以下示例显示了此扩展方法的定义:SpicinessICourseAddTransient<TService>
You already saw this AddTransient method overload previously, when we discussed torn Lifestyles in section 15.1.2. The ChiliConCarne constructor is invoked with a hot Spiciness every time the ICourse service is resolved. The following example shows the definition of this AddTransient<TService> extension method:
public static IServiceCollection AddTransient<TService>(
this IServiceCollection services,
Func<IServiceProvider, TService> implementationFactory)
where TService : class;
如您所见,此AddTransient方法接受 type 的参数Func<IServiceProvider, TService>。对于之前的注册,当 anICourse被解析时,MS.DI 将调用提供的委托并为其提供IServiceProvider属于当前IServiceScope. 有了它,您的代码块就可以解析源自同一个. 我们将在下一节中对此进行演示。IServiceScope
As you can see, this AddTransient method accepts a parameter of type Func<IServiceProvider, TService>. With respect to the previous registration, when an ICourse is resolved, MS.DI will call the supplied delegate and supply it with the IServiceProvider belonging to the current IServiceScope. With it, your code block can resolve instances that originate from the same IServiceScope. We’ll demonstrate this in the next section.
When it comes to the ChiliConCarne class, you have a choice between Auto-Wiring or using a code block. But other classes are more restrictive: they can’t be instantiated through a public constructor. Instead, you must use some sort of factory to create instances of the type. This is always troublesome for DI Containers, because, by default, they look after public constructors. Consider this example constructor for the public JunkFood class:
Even though the JunkFood class might be public, the constructor is internal. In this example, instances of JunkFood should instead be created through the static JunkFoodFactory class:
public static class JunkFoodFactory
{
public static JunkFood Create(string name)
{
return new JunkFood(name);
}
}
From MS.DI’s perspective, this is a problematic API, because there are no unambiguous and well-established conventions around static factories. It needs help — and you can give that help by providing a code block it can execute to create the instance:
This time, you use the AddTransient method to create the component by invoking a static factory within the code block. JunkFoodFactory.Create will be invoked every time IMeal is resolved, and the result will be returned.
If you have to write the code to create the instance, how is this in any way better than invoking the code directly? By using a code block inside a AddTransient method call, you still gain something:
你映射从IMeal到JunkFood。这允许消费类保持松散耦合。
You map from IMeal to JunkFood. This allows consuming classes to stay loosely coupled.
Lifestyles can still be configured. Although the code block will be invoked to create the instance, it may not be invoked every time the instance is requested. It is by default, but if you change it to a Singleton, the code block will only be invoked once, and the result cached and reused thereafter.
In this section, you’ve seen how you can use MS.DI to deal with more-difficult creational APIs. Up until this point, the code examples have been fairly straightforward. This will quickly change when you start to work with multiple components, so let’s now turn our attention in that direction.
As alluded to in section 12.1.2, DI Containers thrive on distinctness but have a hard time with ambiguity. When using Constructor Injection, a single constructor is preferred over overloaded constructors, because it’s evident which constructor to use when there’s no choice. This is also the case when mapping from Abstractions to concrete types. If you attempt to map multiple concrete types to the same Abstraction, you introduce ambiguity.
尽管模棱两可的性质不受欢迎,但您经常需要处理单个抽象的多个实现。在这些情况下可能会出现这种情况:
Despite the undesirable qualities of ambiguity, you often need to work with multiple implementations of a single Abstraction. This can be the case in these situations:
不同的混凝土类型用于不同的消费者。
Different concrete types are used for different consumers.
In this section, we’ll look at each of these cases and see how you can address each with MS.DI. When we’re done, you should have a good feel for what you can do with MS.DI and where the boundaries lie when multiple implementations of the same Abstraction are in play. Let’s first see how you can provide more fine-grained control than Auto-Wiring provides.
Auto-Wiring is convenient and powerful but provides little control. As long as all Abstractions are distinctly mapped to concrete types, you have no problems. But as soon as you introduce more implementations of the same interface, ambiguity rears its ugly head. Let’s first recap how MS.DI deals with multiple registrations of the same Abstraction.
配置同一服务的多个实现
Configuring multiple implementations of the same service
正如您在 15.1.2 节中看到的,您可以注册同一接口的多个实现:
As you saw in section 15.1.2, you can register multiple implementations of the same interface:
This example registers both the Steak and SauceBéarnaise classes as the IIngredient service. The last registration wins, so if you resolve IIngredient with GetRequired—Service<IIngredient>(), you’ll get a Steak instance.
Under the hood, GetServices delegates to GetRequiredService, while requesting an IEnumerable<IIngredient>>. You can also ask the container to resolve all IIngredient components using GetRequiredService instead:
Notice that you use the normal GetRequiredService method, but that you request IEnumerable<IIngredient>. The container interprets this as a convention and gives you all the IIngredient components it has.
When there are multiple implementations of a certain Abstraction, there’ll often be a consumer that depends on a sequence. Sometimes, however, components need to work with a fixed set or a subset of Dependencies of the same Abstraction, which is what we’ll discuss next.
As useful as Auto-Wiring is, sometimes you need to override the normal behavior to provide fine-grained control over which Dependencies go where, but it may also be that you need to address an ambiguous API. As an example, consider this constructor:
public ThreeCourseMeal(ICourse entrée, ICourse mainCourse, ICourse dessert)
In this case, you have three identically typed Dependencies, each of which represents a different concept. In most cases, you want to map each of the Dependencies to a separate type.
As stated previously, when compared to both Autofac and Simple Injector, MS.DI is limited in functionality. Where Autofac provides keyed registrations, and Simple Injector provides conditional registrations to deal with this kind of ambiguity, MS.DI falls short in this respect. There isn’t any built-in functionality to do this. To wire up such an ambiguous API with MS.DI, you have to revert to using a code block.
Listing 15.4 Wiring ThreeCourseMeal by resolving courses in a code block
services.AddTransient<IMeal>(c => new ThreeCourseMeal( ① entrée: c.GetRequiredService<Rillettes>(), ② mainCourse: c.GetRequiredService<CordonBleu>(), ② dessert: c.GetRequiredService<CrèmeBrûlée>())); ②
This registration reverts from Auto-Wiring and constructs the ThreeCourseMeal using a delegate instead. Fortunately, the three ICourse implementations themselves are still Auto-Wired. To bring Auto-Wiring back for the ThreeCourseMeal, you make use of MS.DI’s ActivatorUtilities class.
The lack of Auto-Wiring of ThreeCourseMeal isn’t that problematic in this example because, in this case, you override all constructor arguments. This could be different if ThreeCourseMeal contained more Dependencies:
public ThreeCourseMeal(
ICourse entrée,
ICourse mainCourse,
ICourse dessert,
... ①
)
MS.DI contains a utility class called ActivatorUtilities that allows Auto-Wiring a class’s Dependencies, while overriding other Dependencies by explicitly supplying their values. Using ActivatorUtilities, you can rewrite the previous registration.
Listing 15.5 Wiring ThreeCourseMeal using ActivatorUtilities
services.AddTransient<IMeal>(c =>
ActivatorUtilities.CreateInstance<ThreeCourseMeal>( ① c, ② new object[] ③ { ③ c.GetRequiredService<Rillettes>(), ③ c.GetRequiredService<CordonBleu>(), ③ c.GetRequiredService<MousseAuChocolat>() ③
}));
The CreateInstance<T> method creates a new instance of the supplied T. It goes through the supplied parameters array and matches each parameter to a compatible constructor parameter. Then it resolves the remaining, unmatched constructor parameters with the supplied IServiceProvider.
Because all three resolved courses implement ICourse, there’s still ambiguity in the call. CreateInstance<T> resolves this ambiguity by applying the supplied parameters from left to right. This means that because Rillettes is the first element in the parameters array, it’ll be applied to the first compatible parameter of the ThreeCourseMeal constructor. This is the entrée parameter of type ICourse.
When compared to listing 15.4, there’s a big downside to listing 15.5. Listing 15.4 is verified by the compiler. Any refactoring to the constructor would either allow that code to stay working or fail with a compile error.
The opposite is true with listing 15.5. If the three ICourse constructor parameters are rearranged, code will keep compiling, and ActivatorUtilities would even be able to construct a new ThreeCourseMeal. But unless listing 15.5 is changed according to that rearrangement, the courses are injected in an incorrect order, which will likely cause the application to behave incorrectly. Unfortunately, no refactoring tool will signal that the registration must be changed too.
Even the related registrations of Autofac and Simple Injector (listings 13.7 and 14.9) do a better job of preventing errors. Although neither listing is type-safe, because both listings match on exact parameter names, a change to the ThreeCourseMeal would at least cause an exception when the class is resolved. This is always better than failing silently, which is what could happen in the case of listing 15.5.
通过将参数显式映射到组件来覆盖自动装配是一种普遍适用的解决方案。在使用 Autofac 的命名注册和使用 MS.DI 的 Simple Injector 的条件注册的地方,您可以通过传递手动解析的具体类型来覆盖参数。如果您要管理许多类型,这可能会很脆弱。更好的解决方案是设计自己的 API 来消除这种歧义。它通常会带来更好的整体设计。
Overriding Auto-Wiring by explicitly mapping parameters to components is a universally applicable solution. Where you use named registrations with Autofac and conditional registrations with Simple Injector, with MS.DI, you override parameters by passing in manually resolved concrete types. This can be brittle if you have many types to manage. A better solution is to design your own API to get rid of that ambiguity. It often leads to a better overall design.
In the next section, you’ll see how to use the less ambiguous and more flexible approach where you allow any number of courses in a meal. To this end, you must learn how MS.DI deals with sequences.
In section 6.1.1, we discussed how Constructor Injection acts as a warning system for Single Responsibility Principle violations. The lesson then was that instead of viewing Constructor Over-injection as a weakness of the Constructor Injection pattern, you should rather rejoice that it makes problematic design so obvious.
When it comes to DI Containers and ambiguity, we see a similar relationship. DI Containers generally don’t deal with ambiguity in a graceful manner. Although you can make a DI Container deal with it, it can seem awkward. This is often an indication that you could improve the design of your code.
在本节中,我们将查看一个示例,演示如何通过重构消除歧义。它还将显示 MS.DI 如何处理序列。
In this section, we’ll look at an example that demonstrates how you can refactor away from ambiguity. It’ll also show how MS.DI deals with sequences.
通过消除歧义重构更好的课程
Refactoring to a better course by removing ambiguity
In section 15.4.1, you saw how the ThreeCourseMeal and its inherent ambiguity forced you to either abandon Auto-Wiring or make use of the rather verbose call to ActivatorUtilities. A simple generalization moves toward an implementation of IMeal that takes an arbitrary number of ICourse instances instead of exactly three, as was the case with the ThreeCourseMeal class:
public Meal(IEnumerable<ICourse> courses)
请注意,不需要ICourse在构造函数中使用三个不同的实例,对一个实例的单一依赖IEnumerable<ICourse>可以让您为班级提供任意数量的课程Meal——从零到……很多!这解决了含糊不清的问题,因为现在只有一个Dependency。此外,它还通过提供一个单一的通用类来改进 API 和实现,该类可以模拟不同类型的膳食:从只有一道菜的简单膳食到精心制作的 12 道菜晚餐。
Notice that, instead of requiring three distinct ICourse instances in the constructor, the single dependency on an IEnumerable<ICourse> instance lets you provide any number of courses to the Meal class — from zero to ... a lot! This solves the issue with ambiguity, because there’s now only a single Dependency. In addition, it also improves the API and implementation by providing a single, general-purpose class that can model different types of meal: from a simple meal with a single course to an elaborate 12-course dinner.
In this section, we’ll look at how you can configure MS.DI to wire up Meal instances with appropriate ICourseDependencies. When we’re done, you should have a good idea of the options available when you need to configure instances with sequences of Dependencies.
MS.DI understands sequences, so if you want to use all registered components of a given service, Auto-Wiring just works. As an example, you can configure the IMeal service and its courses like this:
Notice that this is a completely standard mapping from Abstractions to concrete types. MS.DI automatically understands the Meal constructor and determines that the correct course of action is to resolve all ICourse components. When you resolve IMeal, you get a Meal instance with the ICourse components Rillettes, CordonBleu, and MousseAuChocolat.
MS.DI automatically handles sequences, and unless you specify otherwise, it does what you’d expect it to do: it resolves a sequence of Dependencies to all registered components of that type. Only when you need to explicitly pick only some components from a larger set do you need to do more. Let’s see how you can do that.
MS.DI’s default strategy of injecting all components is often the correct policy, but as figure 15.4 shows, there may be cases where you want to pick only some registered components from the larger set of all registered components.
When you previously let MS.DI Auto-Wire all configured instances, it corresponded to the situation depicted on the right side of the figure. If you want to register a component as shown on the left side, you must explicitly define which components should be used. In order to achieve this, you can use the AddTransient method that accepts a delegate. This time around, you’re dealing with the Meal constructor, which only takes a single parameter.
Listing 15.6 Injecting an ICourse subset into Meal
services.AddScoped<Rillettes>(); ① services.AddTransient<LobsterBisque>(); ① services.AddScoped<CordonBleu>(); ① services.AddScoped<OssoBuco>(); ① services.AddSingleton<MousseAuChocolat>(); ① services.AddTransient<CrèmeBrûlée>(); ① services.AddTransient<ICourse>( ② c => c.GetRequiredService<Rillettes>()); ② services.AddTransient<ICourse( ② c => c.GetRequiredService<LobsterBisque>()); ② services.AddTransient<ICourse>( ② c => c.GetRequiredService<CordonBleu>()); ② services.AddTransient<ICourse( ② c => c.GetRequiredService<OssoBuco>()); ② services.AddTransient<ICourse>( ② c => c.GetRequiredService<MousseAuChocolat>()); ② services.AddTransient<ICourse( ② c => c.GetRequiredService<CrèmeBrûlée>()); ②
services.AddTransient<IMeal>(c = new Meal(
new ICourse[] ③ { ③ c.GetRequiredService<Rillettes>(), ③ c.GetRequiredService<CordonBleu>(), ③ c.GetRequiredService<MousseAuChocolat>() ③ })); ③
MS.DI natively understands sequences; unless you need to explicitly pick only some components from all services of a given type, MS.DI automatically does the right thing. Auto-Wiring works not only with single instances, but also for sequences, and the container maps a sequence to all configured instances of the corresponding type. A perhaps less intuitive use of having multiple instances of the same Abstraction is the Decorators design pattern, which we’ll discuss next.
In section 9.1.1, we discussed how the Decorator design pattern is useful when implementing Cross-Cutting Concerns. By definition, Decorators introduce multiple types of the same Abstraction. At the very least, you have two implementations of an Abstraction: the Decorator itself and the decorated type. If you stack the Decorators, you can have even more. This is another example of having multiple registrations of the same service. Unlike the previous sections, these registrations aren’t conceptually equal, but rather Dependencies of each other.
MS.DI has no built-in support for Decorators, and this is one of the areas where the limitations of MS.DI can hinder productivity. Nonetheless, we’ll show how you can, to some degree, work around these limitations.
You can hack around this omission by, again, making use of the ActivatorUtilities class. The following example shows how to use this class to apply Breading to VealCutlet:
services.AddTransient<IIngredient>(c => ① ActivatorUtilities.CreateInstance<Breading>( ①
c,
ActivatorUtilities ② .CreateInstance<VealCutlet>(c))); ②
As you learned in chapter 9, you get veal cordon bleu when you slit open a pocket in the veal cutlet and add ham, cheese, and garlic into the pocket before breading the cutlet. The following example shows how to add a HamCheeseGarlic Decorator in between VealCutlet and the Breading Decorator:
services.AddTransient<IIngredient>(c =>
ActivatorUtilities.CreateInstance<Breading>(
c,
ActivatorUtilities.CreateInstance<HamCheeseGarlic>( ① c,
ActivatorUtilities
.CreateInstance<VealCutlet>(c))));
By making HamCheeseGarlic become a Dependency of Breading, and VealCutlet a Dependency of HamCheeseGarlic, the HamCheeseGarlic Decorator becomes the middle class in the object graph. This results in an object graph equal to the following Pure DI version:
new Breading( ① new HamCheeseGarlic( ① new VealCutlet())); ①
As you might guess, chaining Decorators with MS.DI is cumbersome and verbose. Let’s add insult to injury by taking a look at what happens if you try to apply Decorators to generic Abstractions.
During the course of chapter 10, we defined multiple generic Decorators that could be applied to any ICommandService<TCommand> implementation. In the remainder of this chapter, we’ll set our ingredients and courses aside, and we’ll take a look at how to register these generic Decorators using MS.DI. The following listing demonstrates how to register all ICommandService<TCommand> implementations with the three Decorators presented in section 10.3.
Assembly assembly = typeof(AdjustInventoryService).Assembly;
var mappings =
from type in assembly.GetTypes() ① where !type.IsAbstract ① where !type.IsGenericType ① from i in type.GetInterfaces() ① where i.IsGenericType ① where i.GetGenericTypeDefinition() ① == typeof(ICommandService<>) ① select new { service = i, implementation = type }; ①
foreach (var mapping in mappings)
{
Type commandType = ② mapping.service.GetGenericArguments()[0]; ② Type secureDecoratoryType = ③ typeof(SecureCommandServiceDecorator<>) ③ .MakeGenericType(commandType); ③ Type transactionDecoratorType = ③ typeof(TransactionCommandServiceDecorator<>) ③ .MakeGenericType(commandType); ③ Type auditingDecoratorType = ③ typeof(AuditingCommandServiceDecorator<>) ③ .MakeGenericType(commandType); ③ services.AddTransient(mapping.service, c => ④ ActivatorUtilities.CreateInstance( ④ c, ④ secureDecoratoryType, ④ ActivatorUtilities.CreateInstance( ④ c, ④ transactionDecoratorType, ④ ActivatorUtilities.CreateInstance( ④ c, ④ auditingDecoratorType, ④ ActivatorUtilities.CreateInstance( ④ c, ④ mapping.implementation))))); ④
}
In case you think that listing 15.7 looks rather complicated, unfortunately, this is just the beginning. That listing presents many shortcomings, some of which are difficult to work around. These include the following:
Creation of closed-generic Decorator types can become difficult when either of the generic type arguments of the Decorator don’t exactly match that of the Abstraction.4
不可能添加应用装饰器的开放通用实现,而不必被迫显式地为每个封闭通用抽象进行注册。
It’s impossible to add open-generic implementations that get Decorators applied without being forced to explicitly make the registration for each closed-generic Abstraction.
有条件地应用装饰器,例如,基于泛型类型参数,会变得很复杂。
Applying Decorators conditionally, for instance, based on generic type arguments, gets complicated.
使用替代Lifestyle ,在实现实现多个接口的情况下防止 Torn Lifestyles变得复杂。
With an alternative Lifestyle, it becomes complex to prevent Torn Lifestyles in case an implementation implements multiple interfaces.
很难区分生活方式;链中的所有装饰器都获得相同的Lifestyle。
It’s hard to differentiate Lifestyles; all Decorators in the chain get the same Lifestyle.
You could try working through these limitations one-by-one and suggest improvements to listing 15.7, but you’d effectively be developing a new DI Container on top of MS.DI, which is something we discourage. This wouldn’t be productive. Good alternatives, such as Autofac and Simple Injector, are a better pick for this scenario.5
Although consumers that rely on sequences of Dependencies can be the most intuitive use of multiple instances of the same Abstraction, Decorators are another good example. But there’s a third and perhaps a bit surprising case where multiple instances come into play, which is the Composite design pattern.
During the course of this book, we discussed the Composite design pattern on several occasions. In section 6.1.2, for instance, you created a CompositeNotificationService (listing 6.4) that both implemented INotificationService and wrapped a sequence of INotificationService implementations.
Let’s take a look at how you can register Composites, such as the CompositeNotification—Service of chapter 6 in MS.DI. The following listing shows this class again.
In this example, three INotificationService implementations are registered by their concrete type using the Auto-Wiring API of MS.DI. The CompositeNotificationService, on the other hand, is registered using a delegate. Inside the delegate, the Composite is newed up manually and injected with an array of INotificationService instances. By specifying the concrete types, the previously made registrations are resolved.
Because the number of notification services will likely grow over time, you can reduce the burden on your Composition Root by applying Auto-Registration. Because MS.DI lacks any features in this respect, as we discussed previously, you need to scan the assemblies yourself.
Assembly assembly = typeof(OrderFulfillment).Assembly;
Type[] types = (
from type in assembly.GetTypes()
where !type.IsAbstract
where typeof(INotificationService).IsAssignableFrom(type)
select type)
.ToArray(); ①
foreach (Type type in types)
{
services.AddTransient(type);
}
services.AddTransient<INotificationService>(c =>
new CompositeNotificationService(
types.Select(t =>
(INotificationService)c.GetRequiredService(t))
.ToArray()));
Compared to the Decorator example of listing 15.7, listing 15.9 looks reasonably simple. The assembly is scanned for INotificationService implementations, and each found type is appended to the services collection. The array of types is used by the CompositeNotificationService registration. The Composite is injected with a sequence of INotificationService instances that are resolved by iterating through the array of types.
You might be getting used to the level of complexity and verbosity that you need when dealing with MS.DI, but unfortunately, we’re not done yet. Our LINQ query will register any non-generic implementation that implements INotificationService. When you try to run the previous code, depending on which assembly your Composite is located, MS.DI might throw the following exception:
引发了“System.StackOverflowException”类型的异常。
Exception of type 'System.StackOverflowException' was thrown.
Ouch! Stack overflow exceptions are really painful, because they abort the running process and are hard to debug. Besides, this generic exception gives no detailed information about what caused the stack overflow. Instead, you want MS.DI to throw a descriptive exception explaining the cycle, as both Autofac and Simple Injector do.
This stack overflow exception is caused by a cyclic Dependency in CompositeNotificationService. The Composite is picked up by the LINQ query and resolved as part of the sequence. This results in the Composite being dependent on itself. This is an object graph that’s impossible for MS.DI, or any DI Container for that matter, to construct. CompositeNotificationService became a part of the sequence because our LINQ query found all non-generic INotificationService implementations, which includes the Composite.
There are multiple ways around this. The simplest solution is to move the Composite to a different assembly; for instance, the assembly containing the Composition Root. This prevents the LINQ query from selecting the type. Another option is to filter CompositeNotificationService out of the list:
Type[] types = (
from type in assembly.GetTypes()
where !type.IsAbstract
where typeof(INotificationService)
.IsAssignableFrom(type)
where type != typeof(CompositeNotificationService) ①
select type)
.ToArray();
Composite classes, however, aren’t the only classes that might require removal. You’ll have to do the same for any Decorator. This isn’t particularly difficult, but because there typically will be more Decorator implementations, you might be better off querying the type information to find out whether the type represents a Decorator or not. Here’s how you can filter out Decorators as well:
Type[] types = (
from type in assembly.GetTypes()
where !type.IsAbstract
where typeof(INotificationService).IsAssignableFrom(type)
where type != typeof(CompositeNotificationService)
where type => !IsDecoratorFor<INotificationService>(type)
select type)
.ToArray();
以下代码显示了该IsDecoratorFor方法:
And the following code shows the IsDecoratorFor method:
The IsDecoratorFor method expects a type to have only a single constructor. A type is considered to be a Decorator when it both implements the given TAbstraction and when its constructor also requires a T.
In section 15.4.3, you saw how to register generic Decorators. In this section, we’ll take a look at how you can register Composites for generic Abstractions.
In section 6.1.3, you specified the CompositeEventHandler<TEvent> class (listing 6.12) as a Composite implementation over a sequence of IEventHandler<TEvent> implementations. Let’s see if you can register the Composite with its wrapped event handler implementations. To pull this off in MS.DI, you’ll have to get creative, because you have to work around a few unfortunate limitations.
We found that the easiest way to hide event handler implementations behind a Composite is by not registering those implementations at all, and instead moving the construction of the handlers to the Composite. This isn’t pretty, but it gets the job done. In order to hide handlers behind a Composite, you have to rewrite the CompositeEventHandler<TEvent> implementation of listing 6.12 to that in listing 15.10.
public class CompositeSettings ① { ① public Type[] AllHandlerTypes { get; set; } ① } ①
public class CompositeEventHandler<TEvent>
: IEventHandler<TEvent>
{
private readonly IServiceProvider provider;
private readonly CompositeSettings settings;
public CompositeEventHandler(
IServiceProvider provider, ② CompositeSettings settings) ②
{
this.provider = provider;
this.settings = settings;
}
public void Handle(TEvent e)
{
foreach (var handler in this.GetHandlers()) ③ { ③ handler.Handle(e); ③ } ③
}
IEnumerable<IEventHandler<TEvent>> GetHandlers()
{
return
from type in this.settings.AllHandlerTypes
where typeof(IEventHandler<TEvent>) ④ .IsAssignableFrom(type) ④
select (IEventHandler<TEvent>)
ActivatorUtilities.CreateInstance( ⑤ this.provider, type); ⑤
}
}
Compared to the original implementation of listing 6.12, this Composite implementation is more complex. It also takes a hard dependency on MS.DI itself by making use of its IServiceProvider and ActivatorUtilities. In view of this dependency, this Composite certainly belongs inside the Composition Root, because the rest of the application should stay oblivious to the use of a DI Container.
Instead of depending on an IEventHandler<TEvent> sequence, the Composite depends on a Parameter Object that contains all handler types, which includes types that can’t be cast to the specific closed-generic IEventHandler<TEvent> of the Composite. Because of this, the Composite takes on part of the job that the DI Container is supposed to do. It filters out all incompatible types by calling typeof(IEventHandler<TEvent>).IsAssignableFrom(type). This leaves you with a registration of the Composite and the scanning of all event handlers.
var handlerTypes = ① from type in assembly.GetTypes() ① where !type.IsAbstract ① where !type.IsGenericType ① let serviceTypes = type.GetInterfaces() ① .Where(i => i.IsGenericType && ① i.GetGenericTypeDefinition() ① == typeof(IEventHandler<>)) ① where serviceTypes.Any() ① select type; ① services.AddSingleton(new CompositeSettings ② { ② AllHandlerTypes = handlerTypes.ToArray() ② }); ② services.AddTransient( ③ typeof(IEventHandler<>), ③ typeof(CompositeEventHandler<>)); ③
Even though we’ve managed to work around some of the limitations of MS.DI, you might be less lucky in other cases. For instance, you might run out of luck if the sequence of elements consists of both non-generic and generic implementations, when generic implementations contain generic type constraints or when Decorators need to be conditional.
We do admit that this is an unpleasant solution. We preferred writing less code to show you how to apply MS.DI to the patterns presented in this book, but not all is peaches and cream, unfortunately. That’s why, in our day-to-day development jobs, we prefer Pure DI or one of the mature DI Containers, such as Autofac and Simple Injector.
无论您选择哪种DI Container,或者即使您更喜欢Pure DI,我们都希望本书传达了一个重点——DI 不依赖于特定的技术,例如特定的DI Container。可以而且应该使用本书中介绍的 DI 友好模式和实践来设计应用程序。当您成功做到这一点时,DI 容器的选择就变得不那么重要了。DI 容器是一种组合您的应用程序的工具,但理想情况下,您应该能够用另一个容器替换一个容器,而无需重写应用程序的除组合根之外的任何部分。
No matter which DI Container you select, or even if you prefer Pure DI, we hope that this book has conveyed one important point — DI doesn’t rely on a particular technology, such as a particular DI Container. An application can, and should, be designed using the DI-friendly patterns and practices presented in this book. When you succeed in doing that, selection of a DI Container becomes of less importance. A DI Container is a tool that composes your application, but ideally, you should be able to replace one container with another without rewriting any part of your application other than the Composition Root.
概括
Summary
Microsoft.Extensions.DependencyInjection (MS.DI) DI 容器具有一组有限的功能。缺少解决Auto-Registration、Decorators 和 Composites 的综合 API。这使得它不太适合开发围绕本书中介绍的原则和模式设计的应用程序。
The Microsoft.Extensions.DependencyInjection (MS.DI) DI Container has a limited set of features. A comprehensive API that addresses Auto-Registration, Decorators, and Composites is missing. This makes it less suited for development of applications that are designed around the principles and patterns presented in this book.
MS.DI enforces a strict separation of concerns between configuring and consuming a container. You configure components using a ServiceCollection instance, but a ServiceCollection can’t resolve components. When you’re done configuring a ServiceCollection, you use it to build a ServiceProvider that you can use to resolve components.
With MS.DI, resolving from the root container directly is a bad practice. This will easily lead to memory leaks or concurrency bugs. Instead, you should always resolve from an IServiceScope.
Here are brief definitions of selected terms, patterns, and other concepts discussed in this book. Each definition includes a reference to the chapter or section where the term is discussed in greater detail.
抽象——一个包含接口和(抽象)基类的统一术语。见第 1 章。
Abstraction—A unifying term that encompasses both interfaces and (abstract) base classes. See chapter 1.
环境上下文——一种 DI 反模式,它通过使用静态类成员为组合根外部的应用程序代码提供对易失性依赖项或其行为的全局访问。见第 5.3 节。
Ambient Context—A DI anti-pattern that supplies application code outside the Composition Root with global access to a Volatile Dependency or its behavior by the use of static class members. See section 5.3.
Aspect-Oriented Programming (AOP)—An approach to software that aims to reduce boilerplate code required for implementing Cross-Cutting Concerns and other coding patterns. It does this by implementing such patterns in a single place and applying them to a code base either declaratively or based on convention, without modifying the code itself. See chapter 10.
Auto-Registration—The ability to automatically register components based on a certain convention in a DI Container by scanning one or more assemblies for implementations of desired Abstractions. See section 12.2.3.
Auto-Wiring—The ability to automatically compose an object graph from maps between Abstractions and concrete types by making use of type information supplied by the compiler and the Common Language Runtime. See section 12.1.2.
Captive Dependency—A Dependency that’s inadvertently kept alive for too long, because its consumer was given a lifetime that exceeds the Dependency’s expected lifetime. See section 8.4.1.
Command-Query Separation—The idea that each method should either return a result, but not change the observable state of the system, or change the state, but not produce any value. See section 10.3.3.
Configuration as Code—Allows a DI Container’s configuration to be stored as source code. Each mapping between an Abstraction and a particular implementation is expressed explicitly and directly in code. See section 12.2.2.
Constrained Construction——一种 DI 反模式,它强制某个抽象的所有实现要求它们的构造函数具有相同的签名。见第 5.4 节。
Constrained Construction—A DI anti-pattern that forces all implementations of a certain Abstraction to require their constructors to have an identical signature. See section 5.4.
构造函数注入——一种DI 模式,其中依赖项被静态定义为类构造函数的参数列表。参见第 4.2 节。
Constructor Injection—A DI pattern where Dependencies are statically defined as a list of parameters to the class’s constructor. See section 4.2.
Control Freak — 一种 DI 反模式,您在除Composition Root之外的任何地方都依赖于Volatile Dependency。它与控制反转相反。见第 5.1 节。
Control Freak—A DI anti-pattern where you depend on a Volatile Dependency in any place other than a Composition Root. It’s the opposite of Inversion of Control. See section 5.1.
Cross-Cutting Concern—An aspect of a program that affects a larger part of the application. It’s often a non-functional requirement. Typical examples include logging, auditing, access control, and validation. See chapter 9.
Dependency—In principle, any reference that a module holds to another module. When a module references another module, it depends on it. Informally, the term Dependency is often used instead of the more formal Volatile Dependency. See chapter 1.
Dependency Inversion Principle—This principle states that higher-level modules in your applications shouldn’t depend on lower-level modules; instead, both types should depend on Abstractions. The D in SOLID. See section 3.1.2. See also SOLID.
依赖生命周期——见对象生命周期。
Dependency Lifetime—See Object Lifetime.
DI 容器——一个软件库,它提供 DI 功能并自动执行对象组合、拦截和生命周期管理中涉及的许多任务。它是一个解析和管理对象图的引擎。见第 12 章。
DI Container—A software library that provides DI functionality and automates many of the tasks involved in Object Composition, Interception, and Lifetime Management. It’s an engine that resolves and manages object graphs. See chapter 12.
实体——具有固有的、长期身份的域对象。请参阅第 3.1.2 节。
Entity—A domain object with an inherent, long-term identity. See section 3.1.2.
Interception—The ability to intercept calls between two collaborating components in such a way that you can enrich or change the behavior of the Dependency without the need to change the two collaborators themselves. See chapter 9.
Interface Segregation Principle—This principles states that no client should be forced to depend on methods it doesn’t use. The I in SOLID. See section 6.2.1. See also SOLID.
控制反转——这个概念让框架控制对象的生命周期,而不是直接控制它们。见第 1 章。
Inversion of Control—This concept lets a framework control the lifetime of objects instead of directly controlling them. See chapter 1.
Leaky Abstraction—Even though an Abstraction is defined, the implementation details show through and thus lock the Abstraction to the implementation. See section 6.2.1.
Lifestyle——一种描述Dependency预期生命周期的形式化方式。见第 8 章。
Lifestyle—A formalized way of describing the intended lifetime of a Dependency. See chapter 8.
Liskov Substitution Principle—A software design principle that states that a consumer should be able to use any implementation of an Abstraction without changing the correctness of the system. The L in SOLID. See section 10.2.3. See also SOLID.
Local Default——在与消费者相同的程序集中定义的抽象的默认实现。请参阅第 4.2.2 节。
Local Default—A default implementation of an Abstraction that’s defined in the same assembly as the consumer. See section 4.2.2.
方法注入——一种DI 模式,其中依赖项作为方法参数注入到消费者中。见第 4.3 节。
Method Injection—A DI pattern where Dependencies are injected into the consumer as method parameters. See section 4.3.
对象组合——从不同的模块组合应用程序的概念。见第 7 章。
Object Composition—The concept of composing applications from disparate modules. See chapter 7.
对象生命周期——一般来说,这个术语涵盖了任何对象是如何创建和释放的。在 DI 上下文中,该术语涵盖Dependencies的生命周期。见第 8 章。
Object Lifetime—Generally speaking, this term covers how any object is created and deallocated. In DI context, this term covers the lifetime of Dependencies. See chapter 8.
Open/Closed Principle—This principle states that classes should be open for extensibility, but closed for modification. The O in SOLID. See section 4.4.2. See also SOLID.
属性注入——一种DI 模式,其中依赖项通过可写属性注入到消费者中。见第 4.4 节。
Property Injection—A DI pattern where Dependencies are injected into the consumer via writable properties. See section 4.4.
纯 DI — 在没有DI 容器的情况下应用 DI 的实践。见第 3 部分。
Pure DI—The practice of applying DI without a DI Container. See part 3.
Scoped Lifestyle—A Lifestyle where there’s a single instance within a well-defined scope or request, and instances aren’t shared across scopes. See section 8.3.3.
Seam——应用程序代码中的一个地方,抽象用于分隔模块。见第 1 章。
Seam—A place in application code where Abstractions are used to separate modules. See chapter 1.
服务定位器——一种 DI 反模式,它为组合根之外的应用程序组件提供访问一组无限制的易失性依赖项的权限。参见第 5.2 节。
Service Locator—A DI anti-pattern that supplies application components outside the Composition Root with access to an unbounded set of Volatile Dependencies. See section 5.2.
Single Responsibility Principle—This principle states that a class should have only a single responsibility. The S in SOLID. See section 2.1.3. See also SOLID.
SOLID—An acronym that stands for five fundamental design principles: Single Responsibility Principle, Open/Closed Principle, Liskov Substitution Principle, Interface Segregation Principle, and Dependency Inversion Principle. See chapter 10.
Temporal Coupling—Code smell that occurs when there’s an implicit relationship between two or more members of a class, requiring clients to invoke one member before the other. See section 4.3.2.
可测试性——应用程序对自动化单元测试的敏感程度。见第 1 章。
Testability—The degree to which an application is susceptible to automated unit tests. See chapter 1.
Transient Lifestyle—A Lifestyle where all consumers get their own instance of a Dependency. See section 8.3.2.
易失性依赖性——一种涉及有时可能不受欢迎的副作用的依赖性。这可能包括尚不存在的模块或对其运行时环境有不利要求的模块。这些是 DI 解决的依赖关系。请参阅第 1.3.2 节。
Volatile Dependency—A Dependency that involves side effects that can be undesirable at times. This may include modules that don’t yet exist or that have adverse requirements on its runtime environment. These are the Dependencies that are addressed by DI. See section 1.3.2.
Cwalina, Krzysztof and Brad Abrams, Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Ed. (Addison-Wesley, 2009)